
/*-------------------------------------------------------------------------
 *
 * analyze.c
 *	  Postgres 统计生成器
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/analyze.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>

#include "access/detoast.h"
#include "access/genam.h"
#include "access/multixact.h"
#include "access/relation.h"
#include "access/sysattr.h"
#include "access/table.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/tupconvert.h"
#include "access/visibilitymap.h"
#include "access/xact.h"
#include "catalog/catalog.h"
#include "catalog/index.h"
#include "catalog/indexing.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_statistic_ext.h"
#include "commands/dbcommands.h"
#include "commands/progress.h"
#include "commands/tablecmds.h"
#include "commands/vacuum.h"
#include "common/pg_prng.h"
#include "executor/executor.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "parser/parse_oper.h"
#include "parser/parse_relation.h"
#include "pgstat.h"
#include "postmaster/autovacuum.h"
#include "statistics/extended_stats_internal.h"
#include "statistics/statistics.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "utils/acl.h"
#include "utils/attoptcache.h"
#include "utils/builtins.h"
#include "utils/datum.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/pg_rusage.h"
#include "utils/sampling.h"
#include "utils/sortsupport.h"
#include "utils/spccache.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"


/* 每个索引的分析数据 */
typedef struct AnlIndexData
{
	IndexInfo  *indexInfo;		/* BuildIndexInfo 结果 */
	double		tupleFract;		/* 部分索引的行数比例 */
	VacAttrStats **vacattrstats;	/* 要分析的索引属性 */
	int			attr_cnt;
} AnlIndexData;


/* 默认统计目标（GUC 参数） */
int			default_statistics_target = 100;

/* 一些看起来不值得作为参数传递的变量 */
static MemoryContext anl_context = NULL;
static BufferAccessStrategy vac_strategy;


static void fc_do_analyze_rel(Relation fc_onerel,
						   VacuumParams *fc_params, List *fc_va_cols,
						   AcquireSampleRowsFunc fc_acquirefunc, BlockNumber fc_relpages,
						   bool fc_inh, bool fc_in_outer_xact, int fc_elevel);
static void fc_compute_index_stats(Relation fc_onerel, double fc_totalrows,
								AnlIndexData *fc_indexdata, int fc_nindexes,
								HeapTuple *fc_rows, int fc_numrows,
								MemoryContext fc_col_context);
static VacAttrStats *fc_examine_attribute(Relation fc_onerel, int fc_attnum,
									   Node *fc_index_expr);
static int	fc_acquire_sample_rows(Relation fc_onerel, int fc_elevel,
								HeapTuple *fc_rows, int fc_targrows,
								double *fc_totalrows, double *fc_totaldeadrows);
static int	fc_compare_rows(const void *fc_a, const void *fc_b, void *fc_arg);
static int	fc_acquire_inherited_sample_rows(Relation fc_onerel, int fc_elevel,
										  HeapTuple *fc_rows, int fc_targrows,
										  double *fc_totalrows, double *fc_totaldeadrows);
static void fc_update_attstats(Oid fc_relid, bool fc_inh,
							int fc_natts, VacAttrStats **fc_vacattrstats);
static Datum fc_std_fetch_func(VacAttrStatsP fc_stats, int fc_rownum, bool *fc_isNull);
static Datum fc_ind_fetch_func(VacAttrStatsP fc_stats, int fc_rownum, bool *fc_isNull);


/*
 *	analyze_rel() -- 分析一个关系
 *
 * relid 用于识别要分析的关系。如果提供了关系，则在报告打开/锁定关系的失败时使用其中的名称；一旦我们成功打开了关系，就不要再使用它，因为它可能已经过时。
 */
void analyze_rel(Oid fc_relid, RangeVar *fc_relation,
			VacuumParams *fc_params, List *fc_va_cols, bool fc_in_outer_xact,
			BufferAccessStrategy fc_bstrategy)
{
	Relation	fc_onerel;
	int			fc_elevel;
	AcquireSampleRowsFunc fc_acquirefunc = NULL;
	BlockNumber fc_relpages = 0;

	/* 选择日志级别 */
	if (fc_params->options & VACOPT_VERBOSE)
		fc_elevel = INFO;
	else
		fc_elevel = DEBUG2;

	/* 设置静态变量 */
	vac_strategy = fc_bstrategy;

	/*
	 * 检查用户请求的中止。
	 */
	CHECK_FOR_INTERRUPTS();

	/*
	 * 打开关系，获取 ShareUpdateExclusiveLock，以确保两个
	 * ANALYZE 不会同时在其上运行。（这也会锁定一个
	 * 并发 VACUUM，此时并不重要，但如果我们未来试图在死元组上积累统计信息可能会有影响。）如果关系自我们最后看到它以来已被删除，则我们无需处理它。
	 *
	 * 在这种情况下，请确保仅为 ANALYZE 生成日志。
	 */
	fc_onerel = vacuum_open_relation(fc_relid, fc_relation, fc_params->options & ~(VACOPT_VACUUM),
								  fc_params->log_min_duration >= 0,
								  ShareUpdateExclusiveLock);

	/* 如果关系无法打开或锁定，则退出 */
	if (!fc_onerel)
		return;

	/*
	 * 检查是否需要根据所有权跳过关系。此检查
	 * 也会在手动操作时构建要分析的关系列表时发生，并且在此处需要额外进行，因为 ANALYZE 可能发生在多个事务中，其中关系所有权可能已经在此之间发生变化。请确保在这种情况下仅生成 ANALYZE 的日志。
	 */
	if (!vacuum_is_relation_owner(RelationGetRelid(fc_onerel),
								  fc_onerel->rd_rel,
								  fc_params->options & VACOPT_ANALYZE))
	{
		relation_close(fc_onerel, ShareUpdateExclusiveLock);
		return;
	}

	/*
	 * 默默地忽略作为其他后端的临时表的表 ---
	 * 尝试分析这些没有太大意义，因为它们的内容
	 * 可能在磁盘上不是最新的。（我们在这里不发出警告；这
	 * 只会导致在数据库范围内的 ANALYZE 时出现噪音。）
	 */
	if (RELATION_IS_OTHER_TEMP(fc_onerel))
	{
		relation_close(fc_onerel, ShareUpdateExclusiveLock);
		return;
	}

	/*
	 * 我们可以分析任何表，除了 pg_statistic。参见 update_attstats
	 */
	if (RelationGetRelid(fc_onerel) == StatisticRelationId)
	{
		relation_close(fc_onerel, ShareUpdateExclusiveLock);
		return;
	}

	/*
	 * 检查它是否是可分析的 relkind，并进行适当设置。
	 */
	if (fc_onerel->rd_rel->relkind == RELKIND_RELATION ||
		fc_onerel->rd_rel->relkind == RELKIND_MATVIEW)
	{
		/* 常规表，因此我们将使用常规行获取函数 */
		fc_acquirefunc = fc_acquire_sample_rows;
		/* 还获取常规表的大小 */
		fc_relpages = RelationGetNumberOfBlocks(fc_onerel);
	}
	else if (fc_onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
	{
		/*
		 * 对于外部表，调用 FDW 的钩子函数以查看它是否
		 * 支持分析。
		 */
		FdwRoutine *fc_fdwroutine;
		bool		fc_ok = false;

		fc_fdwroutine = GetFdwRoutineForRelation(fc_onerel, false);

		if (fc_fdwroutine->AnalyzeForeignTable != NULL)
			fc_ok = fc_fdwroutine->AnalyzeForeignTable(fc_onerel,
												 &fc_acquirefunc,
												 &fc_relpages);

		if (!fc_ok)
		{
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- cannot analyze this foreign table",
							RelationGetRelationName(fc_onerel))));
			relation_close(fc_onerel, ShareUpdateExclusiveLock);
			return;
		}
	}
	else if (fc_onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
	{
		/*
		 * 对于分区表，我们希望在下面进行递归分析。
		 */
	}
	else
	{
		/* 如果我们在 VACUUM 中已经抱怨，则无需发出警告 */
		if (!(fc_params->options & VACOPT_VACUUM))
			ereport(WARNING,
					(errmsg("skipping \"%s\" --- cannot analyze non-tables or special system tables",
							RelationGetRelationName(fc_onerel))));
		relation_close(fc_onerel, ShareUpdateExclusiveLock);
		return;
	}

	/*
	 * 好的，让我们开始。首先，初始化进度报告。
	 */
	pgstat_progress_start_command(PROGRESS_COMMAND_ANALYZE,
								  RelationGetRelid(fc_onerel));

	/*
	 * 执行正常的非递归分析。我们可以跳过
	 * 分区表，因为它们不包含任何行。
	 */
	if (fc_onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
		fc_do_analyze_rel(fc_onerel, fc_params, fc_va_cols, fc_acquirefunc,
					   fc_relpages, false, fc_in_outer_xact, fc_elevel);

	/*
	 * 如果有子表，则进行递归分析。
	 */
	if (fc_onerel->rd_rel->relhassubclass)
		fc_do_analyze_rel(fc_onerel, fc_params, fc_va_cols, fc_acquirefunc, fc_relpages,
					   true, fc_in_outer_xact, fc_elevel);

	/*
	 * 现在关闭源关系，但保留锁，以免在我们提交之前
	 * 有人删除它。（如果有人这样做，他们将无法清理
	 * 我们在 pg_statistic 中所做的条目。此外，在提交之前释放锁会让我们暴露于 update_attstats 的并发更新失败。）
	 */
	relation_close(fc_onerel, NoLock);

	pgstat_progress_end_command();
}

/*
 *	do_analyze_rel() -- 分析一个关系，递归或不递归
 *
 * 注意，“acquirefunc”仅在非继承情况下相关。
 * 对于继承情况，acquire_inherited_sample_rows() 确定每个子表的
 * 适当的 acquirefunc。
 */
static void fc_do_analyze_rel(Relation fc_onerel, VacuumParams *fc_params,
			   List *fc_va_cols, AcquireSampleRowsFunc fc_acquirefunc,
			   BlockNumber fc_relpages, bool fc_inh, bool fc_in_outer_xact,
			   int fc_elevel)
{
	int			fc_attr_cnt,
				fc_tcnt,
				fc_i,
				fc_ind;
	Relation   *fc_Irel;
	int			fc_nindexes;
	bool		fc_hasindex;
	VacAttrStats **fc_vacattrstats;
	AnlIndexData *fc_indexdata;
	int			fc_targrows,
				fc_numrows,
				fc_minrows;
	double		fc_totalrows,
				fc_totaldeadrows;
	HeapTuple  *fc_rows;
	PGRUsage	fc_ru0;
	TimestampTz fc_starttime = 0;
	MemoryContext fc_caller_context;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	int			fc_save_nestlevel;
	int64		fc_AnalyzePageHit = VacuumPageHit;
	int64		fc_AnalyzePageMiss = VacuumPageMiss;
	int64		fc_AnalyzePageDirty = VacuumPageDirty;
	PgStat_Counter fc_startreadtime = 0;
	PgStat_Counter fc_startwritetime = 0;

	if (fc_inh)
		ereport(fc_elevel,
				(errmsg("analyzing \"%s.%s\" inheritance tree",
						get_namespace_name(RelationGetNamespace(fc_onerel)),
						RelationGetRelationName(fc_onerel))));
	else
		ereport(fc_elevel,
				(errmsg("analyzing \"%s.%s\"",
						get_namespace_name(RelationGetNamespace(fc_onerel)),
						RelationGetRelationName(fc_onerel))));

	/*
	 * 设置一个工作上下文，以便我们可以轻松释放任何产生的垃圾。
	 */
	anl_context = AllocSetContextCreate(CurrentMemoryContext,
										"Analyze",
										ALLOCSET_DEFAULT_SIZES);
	fc_caller_context = MemoryContextSwitchTo(anl_context);

	/*
	 * 切换到表所有者的用户ID，以便任何索引函数以该用户身份运行。
	 * 同时锁定安全性受限的操作，并安排将 GUC 变量更改限制在此命令中。
	 */
	GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
	SetUserIdAndSecContext(fc_onerel->rd_rel->relowner,
						   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
	fc_save_nestlevel = NewGUCNestLevel();

	/* 仅当 autovacuum 日志记录需要时测量经过时间 */
	if (IsAutoVacuumWorkerProcess() && fc_params->log_min_duration >= 0)
	{
		if (track_io_timing)
		{
			fc_startreadtime = pgStatBlockReadTime;
			fc_startwritetime = pgStatBlockWriteTime;
		}

		pg_rusage_init(&fc_ru0);
		if (fc_params->log_min_duration >= 0)
			fc_starttime = GetCurrentTimestamp();
	}

	/*
	 * 确定需要分析哪些列
	 *
	 * 注意，系统属性永远不会被分析，因此我们在查找阶段直接拒绝它们。
	 * 我们还拒绝重复的列提及。 （我们可以选择忽略重复项，但分析
	 * 一列两次将无效；我们最终会在 pg_statistic 中进行冲突更新。）
	 */
	if (fc_va_cols != NIL)
	{
		Bitmapset  *fc_unique_cols = NULL;
		ListCell   *fc_le;

		fc_vacattrstats = (VacAttrStats **) palloc(list_length(fc_va_cols) *
												sizeof(VacAttrStats *));
		fc_tcnt = 0;
		foreach(fc_le, fc_va_cols)
		{
			char	   *fc_col = strVal(lfirst(fc_le));

			fc_i = attnameAttNum(fc_onerel, fc_col, false);
			if (fc_i == InvalidAttrNumber)
				ereport(ERROR,
						(errcode(ERRCODE_UNDEFINED_COLUMN),
						 errmsg("column \"%s\" of relation \"%s\" does not exist",
								fc_col, RelationGetRelationName(fc_onerel))));
			if (bms_is_member(fc_i, fc_unique_cols))
				ereport(ERROR,
						(errcode(ERRCODE_DUPLICATE_COLUMN),
						 errmsg("column \"%s\" of relation \"%s\" appears more than once",
								fc_col, RelationGetRelationName(fc_onerel))));
			fc_unique_cols = bms_add_member(fc_unique_cols, fc_i);

			fc_vacattrstats[fc_tcnt] = fc_examine_attribute(fc_onerel, fc_i, NULL);
			if (fc_vacattrstats[fc_tcnt] != NULL)
				fc_tcnt++;
		}
		fc_attr_cnt = fc_tcnt;
	}
	else
	{
		fc_attr_cnt = fc_onerel->rd_att->natts;
		fc_vacattrstats = (VacAttrStats **)
			palloc(fc_attr_cnt * sizeof(VacAttrStats *));
		fc_tcnt = 0;
		for (fc_i = 1; fc_i <= fc_attr_cnt; fc_i++)
		{
			fc_vacattrstats[fc_tcnt] = fc_examine_attribute(fc_onerel, fc_i, NULL);
			if (fc_vacattrstats[fc_tcnt] != NULL)
				fc_tcnt++;
		}
		fc_attr_cnt = fc_tcnt;
	}

	/*
	 * 打开关系的所有索引，并查看索引中是否有可分析的列。
	 * 然而，如果 ANALYZE 命令中有显式列列表，我们就不分析索引列。
	 *
	 * 如果我们正在进行递归扫描，我们不想接触父级的索引。
	 * 如果我们正在处理一个分区表，我们需要知道是否有任何索引，
	 * 但我们不想处理它们。
	 */
	if (fc_onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
	{
		List	   *fc_idxs = RelationGetIndexList(fc_onerel);

		fc_Irel = NULL;
		fc_nindexes = 0;
		fc_hasindex = fc_idxs != NIL;
		list_free(fc_idxs);
	}
	else if (!fc_inh)
	{
		vac_open_indexes(fc_onerel, AccessShareLock, &fc_nindexes, &fc_Irel);
		fc_hasindex = fc_nindexes > 0;
	}
	else
	{
		fc_Irel = NULL;
		fc_nindexes = 0;
		fc_hasindex = false;
	}
	fc_indexdata = NULL;
	if (fc_nindexes > 0)
	{
		fc_indexdata = (AnlIndexData *) palloc0(fc_nindexes * sizeof(AnlIndexData));
		for (fc_ind = 0; fc_ind < fc_nindexes; fc_ind++)
		{
			AnlIndexData *fc_thisdata = &fc_indexdata[fc_ind];
			IndexInfo  *fc_indexInfo;

			fc_thisdata->indexInfo = fc_indexInfo = BuildIndexInfo(fc_Irel[fc_ind]);
			fc_thisdata->tupleFract = 1.0; /* 如果部分存在，稍后修复 */
			if (fc_indexInfo->ii_Expressions != NIL && fc_va_cols == NIL)
			{
				ListCell   *fc_indexpr_item = list_head(fc_indexInfo->ii_Expressions);

				fc_thisdata->vacattrstats = (VacAttrStats **)
					palloc(fc_indexInfo->ii_NumIndexAttrs * sizeof(VacAttrStats *));
				fc_tcnt = 0;
				for (fc_i = 0; fc_i < fc_indexInfo->ii_NumIndexAttrs; fc_i++)
				{
					int			fc_keycol = fc_indexInfo->ii_IndexAttrNumbers[fc_i];

					if (fc_keycol == 0)
					{
						/* 找到一个索引表达式 */
						Node	   *fc_indexkey;

						if (fc_indexpr_item == NULL)	/* 不应该发生 */
							elog(ERROR, "too few entries in indexprs list");
						fc_indexkey = (Node *) lfirst(fc_indexpr_item);
						fc_indexpr_item = lnext(fc_indexInfo->ii_Expressions,
											 fc_indexpr_item);
						fc_thisdata->vacattrstats[fc_tcnt] =
							fc_examine_attribute(fc_Irel[fc_ind], fc_i + 1, fc_indexkey);
						if (fc_thisdata->vacattrstats[fc_tcnt] != NULL)
							fc_tcnt++;
					}
				}
				fc_thisdata->attr_cnt = fc_tcnt;
			}
		}
	}

	/*
	 * 确定我们需要采样多少行，使用所有可分析列的最坏情况。
	 * 我们使用 100 行的下限，以避免在 Vitter 的算法中可能出现的溢出。
	 * （注意：在没有可分析列的边缘情况下，这也将是目标。）
	 */
	fc_targrows = 100;
	for (fc_i = 0; fc_i < fc_attr_cnt; fc_i++)
	{
		if (fc_targrows < fc_vacattrstats[fc_i]->minrows)
			fc_targrows = fc_vacattrstats[fc_i]->minrows;
	}
	for (fc_ind = 0; fc_ind < fc_nindexes; fc_ind++)
	{
		AnlIndexData *fc_thisdata = &fc_indexdata[fc_ind];

		for (fc_i = 0; fc_i < fc_thisdata->attr_cnt; fc_i++)
		{
			if (fc_targrows < fc_thisdata->vacattrstats[fc_i]->minrows)
				fc_targrows = fc_thisdata->vacattrstats[fc_i]->minrows;
		}
	}

	/*
	 * 也查看扩展统计对象，因为这些可能定义自定义
	 * 统计目标。因此，我们可能需要采样更多行，然后用足够细节构建
	 * 统计信息。
	 */
	fc_minrows = ComputeExtStatisticsRows(fc_onerel, fc_attr_cnt, fc_vacattrstats);

	if (fc_targrows < fc_minrows)
		fc_targrows = fc_minrows;

	/*
	 * 获取样本行
	 */
	fc_rows = (HeapTuple *) palloc(fc_targrows * sizeof(HeapTuple));
	pgstat_progress_update_param(PROGRESS_ANALYZE_PHASE,
								 fc_inh ? PROGRESS_ANALYZE_PHASE_ACQUIRE_SAMPLE_ROWS_INH :
								 PROGRESS_ANALYZE_PHASE_ACQUIRE_SAMPLE_ROWS);
	if (fc_inh)
		fc_numrows = fc_acquire_inherited_sample_rows(fc_onerel, fc_elevel,
												fc_rows, fc_targrows,
												&fc_totalrows, &fc_totaldeadrows);
	else
		fc_numrows = (*fc_acquirefunc) (fc_onerel, fc_elevel,
								  fc_rows, fc_targrows,
								  &fc_totalrows, &fc_totaldeadrows);

	/*
	 * 计算统计信息。每列计算过程中产生的临时结果
	 * 存储在一个子上下文中。计算例程负责确保它们存储到 VacAttrStats
	 * 结构中的任何内容都在 anl_context 中分配。
	 */
	if (fc_numrows > 0)
	{
		MemoryContext fc_col_context,
					fc_old_context;

		pgstat_progress_update_param(PROGRESS_ANALYZE_PHASE,
									 PROGRESS_ANALYZE_PHASE_COMPUTE_STATS);

		fc_col_context = AllocSetContextCreate(anl_context,
											"Analyze Column",
											ALLOCSET_DEFAULT_SIZES);
		fc_old_context = MemoryContextSwitchTo(fc_col_context);

		for (fc_i = 0; fc_i < fc_attr_cnt; fc_i++)
		{
			VacAttrStats *fc_stats = fc_vacattrstats[fc_i];
			AttributeOpts *fc_aopt;

			fc_stats->rows = fc_rows;
			fc_stats->tupDesc = fc_onerel->rd_att;
			fc_stats->compute_stats(fc_stats,
								 fc_std_fetch_func,
								 fc_numrows,
								 fc_totalrows);

			/*
			 * 如果指定了 n_distinct 选项的适当类型,
			 * 则用相应的值覆盖。
			 */
			fc_aopt = get_attribute_options(fc_onerel->rd_id, fc_stats->attr->attnum);
			if (fc_aopt != NULL)
			{
				float8		fc_n_distinct;

				fc_n_distinct = fc_inh ? fc_aopt->n_distinct_inherited : fc_aopt->n_distinct;
				if (fc_n_distinct != 0.0)
					fc_stats->stadistinct = fc_n_distinct;
			}

			MemoryContextResetAndDeleteChildren(fc_col_context);
		}

		if (fc_nindexes > 0)
			fc_compute_index_stats(fc_onerel, fc_totalrows,
								fc_indexdata, fc_nindexes,
								fc_rows, fc_numrows,
								fc_col_context);

		MemoryContextSwitchTo(fc_old_context);
		MemoryContextDelete(fc_col_context);

		/*
		 * 将完成的统计行发出到 pg_statistic，替换目标列的任何
		 * 先前统计信息。 （如果在 pg_statistic 中有我们没有处理的
		 * 列的统计信息，我们将其保持不变。）
		 */
		fc_update_attstats(RelationGetRelid(fc_onerel), fc_inh,
						fc_attr_cnt, fc_vacattrstats);

		for (fc_ind = 0; fc_ind < fc_nindexes; fc_ind++)
		{
			AnlIndexData *fc_thisdata = &fc_indexdata[fc_ind];

			fc_update_attstats(RelationGetRelid(fc_Irel[fc_ind]), false,
							fc_thisdata->attr_cnt, fc_thisdata->vacattrstats);
		}

		/* 构建扩展统计信息（如果有的话）。 */
		BuildRelationExtStatistics(fc_onerel, fc_inh, fc_totalrows, fc_numrows, fc_rows,
								   fc_attr_cnt, fc_vacattrstats);
	}

	pgstat_progress_update_param(PROGRESS_ANALYZE_PHASE,
								 PROGRESS_ANALYZE_PHASE_FINALIZE_ANALYZE);

	/*
	 * 更新 pg_class 中的页面/元组统计信息……但在进行继承统计时不进行更新。
	 *
	 * 我们假设 VACUUM 尚未在 pg_class.reltuples 中设置值，即使在 VACUUM ANALYZE 期间也是如此。虽然 VACUUM 通常会更新 pg_class，但存在例外。一个 "VACUUM (ANALYZE, INDEX_CLEANUP OFF)" 命令将永远不会更新索引关系的 pg_class 条目。还有可能在 VACUUM 期间，如果索引 AM 在其 amvacuumcleanup() 例程中返回 NULL，则单个索引的 pg_class 条目也不会被更新。
	 */
	if (!fc_inh)
	{
		BlockNumber fc_relallvisible;

		visibilitymap_count(fc_onerel, &fc_relallvisible, NULL);

		/*
		 * 更新表关系的 pg_class。首先进行 CCI，以防 acquirefunc 更新了 pg_class。
		 */
		CommandCounterIncrement();
		vac_update_relstats(fc_onerel,
							fc_relpages,
							fc_totalrows,
							fc_relallvisible,
							fc_hasindex,
							InvalidTransactionId,
							InvalidMultiXactId,
							NULL, NULL,
							fc_in_outer_xact);

		/* 索引也相同 */
		for (fc_ind = 0; fc_ind < fc_nindexes; fc_ind++)
		{
			AnlIndexData *fc_thisdata = &fc_indexdata[fc_ind];
			double		fc_totalindexrows;

			fc_totalindexrows = ceil(fc_thisdata->tupleFract * fc_totalrows);
			vac_update_relstats(fc_Irel[fc_ind],
								RelationGetNumberOfBlocks(fc_Irel[fc_ind]),
								fc_totalindexrows,
								0,
								false,
								InvalidTransactionId,
								InvalidMultiXactId,
								NULL, NULL,
								fc_in_outer_xact);
		}
	}
	else if (fc_onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
	{
		/*
		 * 分区表没有存储，因此除了 reltuples 和 relhasindex 之外，我们不设置其 pg_class 条目的任何字段。
		 */
		CommandCounterIncrement();
		vac_update_relstats(fc_onerel, -1, fc_totalrows,
							0, fc_hasindex, InvalidTransactionId,
							InvalidMultiXactId,
							NULL, NULL,
							fc_in_outer_xact);
	}

	/*
	 * 现在向累积统计系统报告 ANALYZE。对于常规表，只有在不进行继承统计时才执行此操作。对于分区表，只有在进行继承统计时才执行此操作。（无论如何，我们不会因分区表的不继承统计而被调用。）
	 *
	 * 仅当我们分析了所有列时才重置 changes_since_analyze 计数器；否则，自动分析还有工作要做。
	 */
	if (!fc_inh)
		pgstat_report_analyze(fc_onerel, fc_totalrows, fc_totaldeadrows,
							  (fc_va_cols == NIL));
	else if (fc_onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
		pgstat_report_analyze(fc_onerel, 0, 0, (fc_va_cols == NIL));

	/*
	 * 如果这不是 VACUUM ANALYZE 的一部分，则让索引 AM 进行清理。
	 *
	 * 请注意，在 ANALYZE-only 模式下调用时，大多数索引 AM 将 amvacuumcleanup() 视为策略而执行无操作。核心索引 AM 中的唯一例外是 GIN/ginvacuumcleanup()。
	 */
	if (!(fc_params->options & VACOPT_VACUUM))
	{
		for (fc_ind = 0; fc_ind < fc_nindexes; fc_ind++)
		{
			IndexBulkDeleteResult *fc_stats;
			IndexVacuumInfo fc_ivinfo;

			fc_ivinfo.index = fc_Irel[fc_ind];
			fc_ivinfo.analyze_only = true;
			fc_ivinfo.estimated_count = true;
			fc_ivinfo.message_level = fc_elevel;
			fc_ivinfo.num_heap_tuples = fc_onerel->rd_rel->reltuples;
			fc_ivinfo.strategy = vac_strategy;

			fc_stats = index_vacuum_cleanup(&fc_ivinfo, NULL);

			if (fc_stats)
				pfree(fc_stats);
		}
	}

	/* 完成索引的处理 */
	vac_close_indexes(fc_nindexes, fc_Irel, NoLock);

	/* 如果合适，记录该操作 */
	if (IsAutoVacuumWorkerProcess() && fc_params->log_min_duration >= 0)
	{
		TimestampTz fc_endtime = GetCurrentTimestamp();

		if (fc_params->log_min_duration == 0 ||
			TimestampDifferenceExceeds(fc_starttime, fc_endtime,
									   fc_params->log_min_duration))
		{
			long		fc_delay_in_ms;
			double		fc_read_rate = 0;
			double		fc_write_rate = 0;
			StringInfoData fc_buf;

			/*
			 * 通过减去我们上面保存的预分析值来计算在分析过程中发生的页面命中/未命中/脏页的差异。
			 */
			fc_AnalyzePageHit = VacuumPageHit - fc_AnalyzePageHit;
			fc_AnalyzePageMiss = VacuumPageMiss - fc_AnalyzePageMiss;
			fc_AnalyzePageDirty = VacuumPageDirty - fc_AnalyzePageDirty;

			/*
			 * 我们不期望分析耗时超过 25 天，使用 TimestampDifferenceMilliseconds 可简化事务。
			 */
			fc_delay_in_ms = TimestampDifferenceMilliseconds(fc_starttime, fc_endtime);

			/*
			 * 请注意，我们以与 VACUUM 相同的方式报告这些读/写速率，这意味着尽管这里的“平均读速率”实际上对应于页面未命中和随之而来的读取（如果启用的话，这些也被 track_io_timing 捕获），但“平均写速率”实际上是指页面被弄脏的速率，而不是写出。因此，当 I/O 时序仅报告读取时，通常会存在非零的“平均写速率”。
			 *
			 * 目前尚不清楚 ANALYZE 是否会导致 FlushBuffer() 被调用，但我们跟踪并支持报告 I/O 写入时间，以防这种情况发生变化，因为这样做几乎是免费的。
			 */

			if (fc_delay_in_ms > 0)
			{
				fc_read_rate = (double) BLCKSZ * fc_AnalyzePageMiss / (1024 * 1024) /
					(fc_delay_in_ms / 1000.0);
				fc_write_rate = (double) BLCKSZ * fc_AnalyzePageDirty / (1024 * 1024) /
					(fc_delay_in_ms / 1000.0);
			}

			/*
			 * 我们将其拆分，以便在未启用 track_io_timing 时不输出空的 I/O 时序值。
			 */

			initStringInfo(&fc_buf);
			appendStringInfo(&fc_buf, _("automatic analyze of table \"%s.%s.%s\"\n"),
							 get_database_name(MyDatabaseId),
							 get_namespace_name(RelationGetNamespace(fc_onerel)),
							 RelationGetRelationName(fc_onerel));
			if (track_io_timing)
			{
				double		fc_read_ms = (double) (pgStatBlockReadTime - fc_startreadtime) / 1000;
				double		fc_write_ms = (double) (pgStatBlockWriteTime - fc_startwritetime) / 1000;

				appendStringInfo(&fc_buf, _("I/O timings: read: %.3f ms, write: %.3f ms\n"),
								 fc_read_ms, fc_write_ms);
			}
			appendStringInfo(&fc_buf, _("avg read rate: %.3f MB/s, avg write rate: %.3f MB/s\n"),
							 fc_read_rate, fc_write_rate);
			appendStringInfo(&fc_buf, _("buffer usage: %lld hits, %lld misses, %lld dirtied\n"),
							 (long long) fc_AnalyzePageHit,
							 (long long) fc_AnalyzePageMiss,
							 (long long) fc_AnalyzePageDirty);
			appendStringInfo(&fc_buf, _("system usage: %s"), pg_rusage_show(&fc_ru0));

			ereport(LOG,
					(errmsg_internal("%s", fc_buf.data)));

			pfree(fc_buf.data);
		}
	}

	/* 回滚索引函数执行的任何 GUC 更改 */
	AtEOXact_GUC(false, fc_save_nestlevel);

	/* 恢复 userid 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	/* 恢复当前上下文并释放内存 */
	MemoryContextSwitchTo(fc_caller_context);
	MemoryContextDelete(anl_context);
	anl_context = NULL;
}

/*
 * 计算关系索引的统计信息
 */
static void fc_compute_index_stats(Relation fc_onerel, double fc_totalrows,
					AnlIndexData *fc_indexdata, int fc_nindexes,
					HeapTuple *fc_rows, int fc_numrows,
					MemoryContext fc_col_context)
{
	MemoryContext fc_ind_context,
				fc_old_context;
	Datum		fc_values[INDEX_MAX_KEYS];
	bool		fc_isnull[INDEX_MAX_KEYS];
	int			fc_ind,
				fc_i;

	fc_ind_context = AllocSetContextCreate(anl_context,
										"Analyze Index",
										ALLOCSET_DEFAULT_SIZES);
	fc_old_context = MemoryContextSwitchTo(fc_ind_context);

	for (fc_ind = 0; fc_ind < fc_nindexes; fc_ind++)
	{
		AnlIndexData *fc_thisdata = &fc_indexdata[fc_ind];
		IndexInfo  *fc_indexInfo = fc_thisdata->indexInfo;
		int			fc_attr_cnt = fc_thisdata->attr_cnt;
		TupleTableSlot *fc_slot;
		EState	   *fc_estate;
		ExprContext *fc_econtext;
		ExprState  *fc_predicate;
		Datum	   *fc_exprvals;
		bool	   *fc_exprnulls;
		int			fc_numindexrows,
					fc_tcnt,
					fc_rowno;
		double		fc_totalindexrows;

		/* 忽略无列可分析和非部分的索引 */  
		if (fc_attr_cnt == 0 && fc_indexInfo->ii_Predicate == NIL)
			continue;

		/*  
		 * 需要一个EState来评估索引表达式和部分索引谓词。  
		 * 在每个索引的上下文中创建它，以确保它在循环末尾被清理。  
		 */  
		fc_estate = CreateExecutorState();
		fc_econtext = GetPerTupleExprContext(fc_estate);
		/* 还需要一个槽来保存当前的堆元组 */  
		fc_slot = MakeSingleTupleTableSlot(RelationGetDescr(fc_onerel),
										&TTSOpsHeapTuple);

		/* 安排econtext的扫描元组为待测试的元组 */
		fc_econtext->ecxt_scantuple = fc_slot;

		/* 为谓词设置执行状态。 */  
		fc_predicate = ExecPrepareQual(fc_indexInfo->ii_Predicate, fc_estate);

		/* 计算并保存索引表达式值 */  
		fc_exprvals = (Datum *) palloc(fc_numrows * fc_attr_cnt * sizeof(Datum));
		fc_exprnulls = (bool *) palloc(fc_numrows * fc_attr_cnt * sizeof(bool));
		fc_numindexrows = 0;
		fc_tcnt = 0;
		for (fc_rowno = 0; fc_rowno < fc_numrows; fc_rowno++)
		{
			HeapTuple	fc_heapTuple = fc_rows[fc_rowno];

			vacuum_delay_point();

			/*  
			 * 每次重置每个元组的上下文，以回收因评估谓词或索引表达式而留下的任何杂物。  
			 */  
			ResetExprContext(fc_econtext);

			/* 为谓词或表达式评估做好准备 */
			ExecStoreHeapTuple(fc_heapTuple, fc_slot, false);

			/* 如果索引是部分的，检查谓词 */  
			if (fc_predicate != NULL)
			{
				if (!ExecQual(fc_predicate, fc_econtext))
					continue;
			}
			fc_numindexrows++;

			if (fc_attr_cnt > 0)
			{
				/*  
				 * 评估索引行以计算表达式值。我们  
				 * 可以手动完成这个，但FormIndexDatum更方便。  
				 */  
				FormIndexDatum(fc_indexInfo,
							   fc_slot,
							   fc_estate,
							   fc_values,
							   fc_isnull);

				/*  
				 * 仅保存我们关心的列。我们将值  
				 * 从estate的每个元组上下文复制到ind_context。  
				 */  
				for (fc_i = 0; fc_i < fc_attr_cnt; fc_i++)
				{
					VacAttrStats *fc_stats = fc_thisdata->vacattrstats[fc_i];
					int			fc_attnum = fc_stats->attr->attnum;

					if (fc_isnull[fc_attnum - 1])
					{
						fc_exprvals[fc_tcnt] = (Datum) 0;
						fc_exprnulls[fc_tcnt] = true;
					}
					else
					{
						fc_exprvals[fc_tcnt] = datumCopy(fc_values[fc_attnum - 1],
												   fc_stats->attrtype->typbyval,
												   fc_stats->attrtype->typlen);
						fc_exprnulls[fc_tcnt] = false;
					}
					fc_tcnt++;
				}
			}
		}

		/*  
		 * 在样本中计算通过谓词的行数后，我们可以估计索引中的总行数。  
		 */  
		fc_thisdata->tupleFract = (double) fc_numindexrows / (double) fc_numrows;
		fc_totalindexrows = ceil(fc_thisdata->tupleFract * fc_totalrows);

		/*  
		 * 现在我们可以计算表达式列的统计数据。  
		 */  
		if (fc_numindexrows > 0)
		{
			MemoryContextSwitchTo(fc_col_context);
			for (fc_i = 0; fc_i < fc_attr_cnt; fc_i++)
			{
				VacAttrStats *fc_stats = fc_thisdata->vacattrstats[fc_i];

				fc_stats->exprvals = fc_exprvals + fc_i;
				fc_stats->exprnulls = fc_exprnulls + fc_i;
				fc_stats->rowstride = fc_attr_cnt;
				fc_stats->compute_stats(fc_stats,
									 fc_ind_fetch_func,
									 fc_numindexrows,
									 fc_totalindexrows);

				MemoryContextResetAndDeleteChildren(fc_col_context);
			}
		}

		/* 并进行清理 */
		MemoryContextSwitchTo(fc_ind_context);

		ExecDropSingleTupleTableSlot(fc_slot);
		FreeExecutorState(fc_estate);
		MemoryContextResetAndDeleteChildren(fc_ind_context);
	}

	MemoryContextSwitchTo(fc_old_context);
	MemoryContextDelete(fc_ind_context);
}

/* examine_attribute -- 单列的前分析  
 *  
 * 确定该列是否可分析；如果可以，则为其创建和初始化  
 * VacAttrStats结构。如果不可以，则返回NULL。  
 *  
 * 如果index_expr不是NULL，那么我们正在尝试分析一个表达式索引，  
 * index_expr是表示列数据的表达式树。  
 */  
static VacAttrStats * fc_examine_attribute(Relation fc_onerel, int fc_attnum, Node *fc_index_expr)
{
	Form_pg_attribute fc_attr = TupleDescAttr(fc_onerel->rd_att, fc_attnum - 1);
	HeapTuple	fc_typtuple;
	VacAttrStats *fc_stats;
	int			fc_i;
	bool		fc_ok;

	/* 永远不要分析已删除的列 */  
	if (fc_attr->attisdropped)
		return NULL;

	/* 如果用户指定不分析，则不要分析该列 */  
	if (fc_attr->attstattarget == 0)
		return NULL;

	/*  
	 * 创建VacAttrStats结构。注意我们只有pg_attribute元组  
	 * 的固定字段的副本。  
	 */  
	fc_stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats));
	fc_stats->attr = (Form_pg_attribute) palloc(ATTRIBUTE_FIXED_PART_SIZE);
	memcpy(fc_stats->attr, fc_attr, ATTRIBUTE_FIXED_PART_SIZE);

	/*  
	 * 当分析表达式索引时，相信表达式树的类型  
	 * 而不是列的数据类型 --- 后者可能是  
	 * opclass的opckeytype存储类型，这对我们的目的  
	 * 并不重要。（注意：如果我们对非表达式索引列进行任何操作，  
	 * 我们需要弄清楚从哪里获取正确的类型信息，  
	 * 但目前这不是问题。）不清楚是否有谁会关心  
	 * typmod，但我们也存储这个以防万一。  
	 */  
	if (fc_index_expr)
	{
		fc_stats->attrtypid = exprType(fc_index_expr);
		fc_stats->attrtypmod = exprTypmod(fc_index_expr);

		/*  
		 * 如果为索引列指定了排序规则，则优先使用该规则  
		 * 的任何其他内容；但如果没有，则回退到我们  
		 * 从表达式中获取的任何内容。  
		 */  
		if (OidIsValid(fc_onerel->rd_indcollation[fc_attnum - 1]))
			fc_stats->attrcollid = fc_onerel->rd_indcollation[fc_attnum - 1];
		else
			fc_stats->attrcollid = exprCollation(fc_index_expr);
	}
	else
	{
		fc_stats->attrtypid = fc_attr->atttypid;
		fc_stats->attrtypmod = fc_attr->atttypmod;
		fc_stats->attrcollid = fc_attr->attcollation;
	}

	fc_typtuple = SearchSysCacheCopy1(TYPEOID,
								   ObjectIdGetDatum(fc_stats->attrtypid));
	if (!HeapTupleIsValid(fc_typtuple))
		elog(ERROR, "cache lookup failed for type %u", fc_stats->attrtypid);
	fc_stats->attrtype = (Form_pg_type) GETSTRUCT(fc_typtuple);
	fc_stats->anl_context = anl_context;
	fc_stats->tupattnum = fc_attnum;

	/*  
	 * 描述stats->stavalues[n]元素类型的字段默认为  
	 * 正在分析的数据的类型，但特定类型的typanalyze  
	 * 函数可以在存储其他内容时更改它们。  
	 */  
	for (fc_i = 0; fc_i < STATISTIC_NUM_SLOTS; fc_i++)
	{
		fc_stats->statypid[fc_i] = fc_stats->attrtypid;
		fc_stats->statyplen[fc_i] = fc_stats->attrtype->typlen;
		fc_stats->statypbyval[fc_i] = fc_stats->attrtype->typbyval;
		fc_stats->statypalign[fc_i] = fc_stats->attrtype->typalign;
	}

	/*  
	 * 调用特定类型的typanalyze函数。如果未指定，则使用  
	 * std_typanalyze()。  
	 */  
	if (OidIsValid(fc_stats->attrtype->typanalyze))
		fc_ok = DatumGetBool(OidFunctionCall1(fc_stats->attrtype->typanalyze,
										   PointerGetDatum(fc_stats)));
	else
		fc_ok = std_typanalyze(fc_stats);

	if (!fc_ok || fc_stats->compute_stats == NULL || fc_stats->minrows <= 0)
	{
		heap_freetuple(fc_typtuple);
		pfree(fc_stats->attr);
		pfree(fc_stats);
		return NULL;
	}

	return fc_stats;
}

/*
 * acquire_sample_rows -- 从表中获取随机样本行
 *
 * 选定的行将返回在调用者分配的数组 rows[] 中，
 * 该数组必须至少有 targrows 个条目。
 * 实际选中的行数作为函数结果返回。
 * 我们还估计表中的活行和死行的总数，
 * 分别将它们返回到 *totalrows 和 *totaldeadrows 中。
 *
 * 返回的元组列表按表中的物理位置排序。
 * （我们将依赖这一点来推导相关性估计。）
 *
 * 从 2004 年 5 月起，我们使用一种新的两阶段方法：第一阶段选择
 * 多达 targrows 的随机块（或者如果没有这么多，则选择所有块）。
 * 第二阶段扫描这些块，并使用 Vitter 算法创建
 * 多达 targrows 行的随机样本（或更少，如果在
 * 块样本中行数更少）。这两个阶段同时执行：每个
 * 块在第一阶段返回其数量后立即处理，而在读取行时第二阶段控制
 * 应该插入样本中的哪些行。
 *
 * 尽管每一行都有同等机会最终进入最终样本，但这种抽样方法并不完美：
 * 并非每个可能的样本都有相等的被选择的机会。对于大型关系，
 * 样本所代表的不同块的数量往往太少。对此我们现在可以接受。
 * 欢迎改进。
 *
 * 这种抽样方法的一个重要特性是，由于我们确实查看了一个
 * 统计上无偏的块集，因此我们应该能够获得
 * 每个块的活行和死行的平均数的无偏估计。
 * 之前的抽样方法对表开头附近的行密度过于信任。
 */
static int fc_acquire_sample_rows(Relation fc_onerel, int fc_elevel,
					HeapTuple *fc_rows, int fc_targrows,
					double *fc_totalrows, double *fc_totaldeadrows)
{
	int			fc_numrows = 0;	/* 当前储存器中的行数 */
	double		fc_samplerows = 0; /* 收集的总行数 */
	double		fc_liverows = 0;	/* 看到的活行数 */
	double		fc_deadrows = 0;	/* 看到的死行数 */
	double		fc_rowstoskip = -1;	/* -1 意味着尚未设置 */
	uint32		fc_randseed;		/* 块采样器的种子 */
	BlockNumber fc_totalblocks;
	TransactionId fc_OldestXmin;
	BlockSamplerData fc_bs;
	ReservoirStateData fc_rstate;
	TupleTableSlot *fc_slot;
	TableScanDesc fc_scan;
	BlockNumber fc_nblocks;
	BlockNumber fc_blksdone = 0;
#ifdef USE_PREFETCH
	int			fc_prefetch_maximum = 0;	/* 如果启用，则预取的块 */
	BlockSamplerData fc_prefetch_bs;
#endif

	Assert(fc_targrows > 0);

	fc_totalblocks = RelationGetNumberOfBlocks(fc_onerel);

	/* 需要一个用于 HeapTupleSatisfiesVacuum 的切割 xmin */
	fc_OldestXmin = GetOldestNonRemovableTransactionId(fc_onerel);

	/* 为样本块编号做准备 */
	fc_randseed = pg_prng_uint32(&pg_global_prng_state);
	fc_nblocks = BlockSampler_Init(&fc_bs, fc_totalblocks, fc_targrows, fc_randseed);

#ifdef USE_PREFETCH
	fc_prefetch_maximum = get_tablespace_maintenance_io_concurrency(fc_onerel->rd_rel->reltablespace);
	/* 使用相同的种子创建另一个 BlockSampler，以便进行预取 */
	if (fc_prefetch_maximum)
		(void) BlockSampler_Init(&fc_prefetch_bs, fc_totalblocks, fc_targrows, fc_randseed);
#endif

	/* 报告样本块编号 */
	pgstat_progress_update_param(PROGRESS_ANALYZE_BLOCKS_TOTAL,
								 fc_nblocks);

	/* 为采样行做准备 */
	reservoir_init_selection_state(&fc_rstate, fc_targrows);

	fc_scan = table_beginscan_analyze(fc_onerel);
	fc_slot = table_slot_create(fc_onerel, NULL);

#ifdef USE_PREFETCH

	/*
	 * 如果我们在进行预取，则继续告诉内核我们
	 * 将要想要的第一组页面。这也将我们的
	 * 迭代器移到主迭代器之前，我们将保持在这里，以便
	 * 我们始终预取出 prefetch_maximum 数量的块
	 * 在前面。
	 */
	if (fc_prefetch_maximum)
	{
		for (int fc_i = 0; fc_i < fc_prefetch_maximum; fc_i++)
		{
			BlockNumber fc_prefetch_block;

			if (!BlockSampler_HasMore(&fc_prefetch_bs))
				break;

			fc_prefetch_block = BlockSampler_Next(&fc_prefetch_bs);
			PrefetchBuffer(fc_scan->rs_rd, MAIN_FORKNUM, fc_prefetch_block);
		}
	}
#endif

	/* 外层循环遍历要抽样的块 */
	while (BlockSampler_HasMore(&fc_bs))
	{
		bool		fc_block_accepted;
		BlockNumber fc_targblock = BlockSampler_Next(&fc_bs);
#ifdef USE_PREFETCH
		BlockNumber fc_prefetch_targblock = InvalidBlockNumber;

		/*
		 * 确保每次主 BlockSampler 向前移动时，
		 * 我们的预取 BlockSampler 也向前移动，以便我们
		 * 始终保持在前面。
		 */
		if (fc_prefetch_maximum && BlockSampler_HasMore(&fc_prefetch_bs))
			fc_prefetch_targblock = BlockSampler_Next(&fc_prefetch_bs);
#endif

		vacuum_delay_point();

		fc_block_accepted = table_scan_analyze_next_block(fc_scan, fc_targblock, vac_strategy);

#ifdef USE_PREFETCH

		/*
		 * 当预取时，在我们获得一个块后，告诉内核我们将要想要的下一个块，如果还有剩余的话。
		 *
		 * 即使上面的 table_scan_analyze_next_block() 调用决定不分析它选择的块，我们也想这样做。
		 */
		if (fc_prefetch_maximum && fc_prefetch_targblock != InvalidBlockNumber)
			PrefetchBuffer(fc_scan->rs_rd, MAIN_FORKNUM, fc_prefetch_targblock);
#endif

		/*
		 * 如果 table_scan_analyze_next_block() 指出这个块不适合分析，则不要分析。
		 */
		if (!fc_block_accepted)
			continue;

		while (table_scan_analyze_next_tuple(fc_scan, fc_OldestXmin, &fc_liverows, &fc_deadrows, fc_slot))
		{
			/*
			 * 前 targrows 的样本行被简单地复制到Reservoir中。然后我们开始在样本中替换元组，直到到达关系的末尾。这个算法来自杰夫·维特的论文（请参见 utils/misc/sampling.c 中的完整引用）。它通过重复计算在选择元组之前需要跳过的元组数来工作，这会替换Reservoir（当前元组集）中随机选择的元素。此时，Reservoir始终是我们迄今为止已通过的元组的真实随机样本，因此当我们超出关系的末尾时，我们完成了。
			 */
			if (fc_numrows < fc_targrows)
				fc_rows[fc_numrows++] = ExecCopySlotHeapTuple(fc_slot);
			else
			{
				/*
				 * Vitter论文中的 t 是已经处理的记录数。如果我们需要计算一个新的 S 值，我们必须使用尚未递增的 samplerows 值作为 t。
				 */
				if (fc_rowstoskip < 0)
					fc_rowstoskip = reservoir_get_next_S(&fc_rstate, fc_samplerows, fc_targrows);

				if (fc_rowstoskip <= 0)
				{
					/*
					 * 找到一个合适的元组，所以保存它，随机替换一个旧元组
					 */
					int			fc_k = (int) (fc_targrows * sampler_random_fract(&fc_rstate.randstate));

					Assert(fc_k >= 0 && fc_k < fc_targrows);
					heap_freetuple(fc_rows[fc_k]);
					fc_rows[fc_k] = ExecCopySlotHeapTuple(fc_slot);
				}

				fc_rowstoskip -= 1;
			}

			fc_samplerows += 1;
		}

		pgstat_progress_update_param(PROGRESS_ANALYZE_BLOCKS_DONE,
									 ++fc_blksdone);
	}

	ExecDropSingleTupleTableSlot(fc_slot);
	table_endscan(fc_scan);

	/*
	 * 如果我们没有找到想要的那么多元组，那么我们就完成了。不需要排序，因为它们已经按顺序排列。
	 *
	 * 否则，我们需要按位置（itempointer）对收集的元组进行排序。对于元组已经排序的特殊情况，不值得担心。
	 */
	if (fc_numrows == fc_targrows)
		qsort_interruptible((void *) fc_rows, fc_numrows, sizeof(HeapTuple),
							fc_compare_rows, NULL);

	/*
	 * 估计关系中活行和死行的总数，推断的假设是我们未扫描的页面中的平均元组密度与我们扫描的页面相同。由于我们扫描的内容是关系中页面的随机样本，这应该是一个不错的假设。
	 */
	if (fc_bs.m > 0)
	{
		*fc_totalrows = floor((fc_liverows / fc_bs.m) * fc_totalblocks + 0.5);
		*fc_totaldeadrows = floor((fc_deadrows / fc_bs.m) * fc_totalblocks + 0.5);
	}
	else
	{
		*fc_totalrows = 0.0;
		*fc_totaldeadrows = 0.0;
	}

	/*
	 * 输出一些有趣的关系信息
	 */
	ereport(fc_elevel,
			(errmsg("\"%s\": scanned %d of %u pages, "
					"containing %.0f live rows and %.0f dead rows; "
					"%d rows in sample, %.0f estimated total rows",
					RelationGetRelationName(fc_onerel),
					fc_bs.m, fc_totalblocks,
					fc_liverows, fc_deadrows,
					fc_numrows, *fc_totalrows)));

	return fc_numrows;
}

/*
 * 用于排序 rows[] 数组的比较器
 */
static int fc_compare_rows(const void *fc_a, const void *fc_b, void *fc_arg)
{
	HeapTuple	fc_ha = *(const HeapTuple *) fc_a;
	HeapTuple	fc_hb = *(const HeapTuple *) fc_b;
	BlockNumber fc_ba = ItemPointerGetBlockNumber(&fc_ha->t_self);
	OffsetNumber fc_oa = ItemPointerGetOffsetNumber(&fc_ha->t_self);
	BlockNumber fc_bb = ItemPointerGetBlockNumber(&fc_hb->t_self);
	OffsetNumber fc_ob = ItemPointerGetOffsetNumber(&fc_hb->t_self);

	if (fc_ba < fc_bb)
		return -1;
	if (fc_ba > fc_bb)
		return 1;
	if (fc_oa < fc_ob)
		return -1;
	if (fc_oa > fc_ob)
		return 1;
	return 0;
}


/*
 * acquire_inherited_sample_rows -- 从继承树中获取样本行
 *
 * 这与 acquire_sample_rows 具有相同的 API，不同之处在于行是从所有继承子节点以及指定表中收集的。
 * 如果没有继承子节点，或者所有子节点都是不支持 ANALYZE 的外部表，我们将失败并返回零。
 */
static int fc_acquire_inherited_sample_rows(Relation fc_onerel, int fc_elevel,
							  HeapTuple *fc_rows, int fc_targrows,
							  double *fc_totalrows, double *fc_totaldeadrows)
{
	List	   *fc_tableOIDs;
	Relation   *fc_rels;
	AcquireSampleRowsFunc *fc_acquirefuncs;
	double	   *fc_relblocks;
	double		fc_totalblocks;
	int			fc_numrows,
				fc_nrels,
				fc_i;
	ListCell   *fc_lc;
	bool		fc_has_child;

	/* 现在初始化输出参数为零，以防我们提前退出 */
	*fc_totalrows = 0;
	*fc_totaldeadrows = 0;

	/*
	 * 查找继承集的所有成员。我们只需要对孩子们进行 AccessShareLock。
	 */
	fc_tableOIDs =
		find_all_inheritors(RelationGetRelid(fc_onerel), AccessShareLock, NULL);

	/*
	 * 检查是否至少有一个后代，否则失败。尽管 analyze_rel 的 relhassubclass 检查，如果表曾经有一个子类但现在没有，这种情况也可能发生。在这种情况下，我们可以清除 relhassubclass 字段，以免以后再犯同样的错误。（这是安全的，因为我们持有 ShareUpdateExclusiveLock。）
	 */
	if (list_length(fc_tableOIDs) < 2)
	{
		/* CCI，因为我们已经在这个命令中更新了 pg_class 行 */
		CommandCounterIncrement();
		SetRelationHasSubclass(RelationGetRelid(fc_onerel), false);
		ereport(fc_elevel,
				(errmsg("skipping analyze of \"%s.%s\" inheritance tree --- this inheritance tree contains no child tables",
						get_namespace_name(RelationGetNamespace(fc_onerel)),
						RelationGetRelationName(fc_onerel))));
		return 0;
	}

	/*
	 * 确定要使用的 acquirefuncs，并计算所有关系中的块数。
	 * 结果可能会超出 BlockNumber，因此我们使用双精度算术。
	 */
	fc_rels = (Relation *) palloc(list_length(fc_tableOIDs) * sizeof(Relation));
	fc_acquirefuncs = (AcquireSampleRowsFunc *)
		palloc(list_length(fc_tableOIDs) * sizeof(AcquireSampleRowsFunc));
	fc_relblocks = (double *) palloc(list_length(fc_tableOIDs) * sizeof(double));
	fc_totalblocks = 0;
	fc_nrels = 0;
	fc_has_child = false;
	foreach(fc_lc, fc_tableOIDs)
	{
		Oid			fc_childOID = lfirst_oid(fc_lc);
		Relation	fc_childrel;
		AcquireSampleRowsFunc fc_acquirefunc = NULL;
		BlockNumber fc_relpages = 0;

		/* 我们已经获得了所需的锁 */
		fc_childrel = table_open(fc_childOID, NoLock);

		/* 如果是另一个后端的临时表则忽略 */
		if (RELATION_IS_OTHER_TEMP(fc_childrel))
		{
			/* ... 但是释放对此的锁 */
			Assert(fc_childrel != fc_onerel);
			table_close(fc_childrel, AccessShareLock);
			continue;
		}

		/* 检查表类型（MATVIEW 不会发生，但也可以允许） */
		if (fc_childrel->rd_rel->relkind == RELKIND_RELATION ||
			fc_childrel->rd_rel->relkind == RELKIND_MATVIEW)
		{
			/* 普通表，因此使用常规行获取函数 */
			fc_acquirefunc = fc_acquire_sample_rows;
			fc_relpages = RelationGetNumberOfBlocks(fc_childrel);
		}
		else if (fc_childrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
		{
			/*
			 * 对于外部表，调用 FDW 的钩子函数以查看
			 * 是否支持分析。
			 */
			FdwRoutine *fc_fdwroutine;
			bool		fc_ok = false;

			fc_fdwroutine = GetFdwRoutineForRelation(fc_childrel, false);

			if (fc_fdwroutine->AnalyzeForeignTable != NULL)
				fc_ok = fc_fdwroutine->AnalyzeForeignTable(fc_childrel,
													 &fc_acquirefunc,
													 &fc_relpages);

			if (!fc_ok)
			{
				/* 忽略，但释放对此的锁 */
				Assert(fc_childrel != fc_onerel);
				table_close(fc_childrel, AccessShareLock);
				continue;
			}
		}
		else
		{
			/*
			 * 忽略，但释放对此的锁。不要尝试解锁
			 * 传入的关系
			 */
			Assert(fc_childrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
			if (fc_childrel != fc_onerel)
				table_close(fc_childrel, AccessShareLock);
			else
				table_close(fc_childrel, NoLock);
			continue;
		}

		/* 好的，我们将处理这个子项 */
		fc_has_child = true;
		fc_rels[fc_nrels] = fc_childrel;
		fc_acquirefuncs[fc_nrels] = fc_acquirefunc;
		fc_relblocks[fc_nrels] = (double) fc_relpages;
		fc_totalblocks += (double) fc_relpages;
		fc_nrels++;
	}

	/*
	 * 如果我们没有至少一个子表可考虑，则失败。如果关系是一个分区表，
	 * 它不算作子表。
	 */
	if (!fc_has_child)
	{
		ereport(fc_elevel,
				(errmsg("skipping analyze of \"%s.%s\" inheritance tree --- this inheritance tree contains no analyzable child tables",
						get_namespace_name(RelationGetNamespace(fc_onerel)),
						RelationGetRelationName(fc_onerel))));
		return 0;
	}

	/*
	 * 现在从每个关系中按比例抽取行，比例为其占总块数的分数。
	 * （如果子关系具有截然不同的空闲空间百分比，这可能不太理想，但
	 * 不清楚是否值得费力去做。）
	 */
	pgstat_progress_update_param(PROGRESS_ANALYZE_CHILD_TABLES_TOTAL,
								 fc_nrels);
	fc_numrows = 0;
	for (fc_i = 0; fc_i < fc_nrels; fc_i++)
	{
		Relation	fc_childrel = fc_rels[fc_i];
		AcquireSampleRowsFunc fc_acquirefunc = fc_acquirefuncs[fc_i];
		double		fc_childblocks = fc_relblocks[fc_i];

		/*
		 * 报告进度。抽样函数通常会报告已完成/总块数，但我们需要在这里将
		 * 它们重置为 0，以便在此之前不会显示旧值。
		 */
		{
			const int	fc_progress_index[] = {
				PROGRESS_ANALYZE_CURRENT_CHILD_TABLE_RELID,
				PROGRESS_ANALYZE_BLOCKS_DONE,
				PROGRESS_ANALYZE_BLOCKS_TOTAL
			};
			const int64 fc_progress_vals[] = {
				RelationGetRelid(fc_childrel),
				0,
				0,
			};

			pgstat_progress_update_multi_param(3, fc_progress_index, fc_progress_vals);
		}

		if (fc_childblocks > 0)
		{
			int			fc_childtargrows;

			fc_childtargrows = (int) rint(fc_targrows * fc_childblocks / fc_totalblocks);
			/* 确保我们不会由于舍入误差而溢出 */
			fc_childtargrows = Min(fc_childtargrows, fc_targrows - fc_numrows);
			if (fc_childtargrows > 0)
			{
				int			fc_childrows;
				double		fc_trows,
							fc_tdrows;

				/* 获取子项行的随机样本 */
				fc_childrows = (*fc_acquirefunc) (fc_childrel, fc_elevel,
											fc_rows + fc_numrows, fc_childtargrows,
											&fc_trows, &fc_tdrows);

				/* 我们可能需要将子项的行类型转换为父项的 */
				if (fc_childrows > 0 &&
					!equalTupleDescs(RelationGetDescr(fc_childrel),
									 RelationGetDescr(fc_onerel)))
				{
					TupleConversionMap *fc_map;

					fc_map = convert_tuples_by_name(RelationGetDescr(fc_childrel),
												 RelationGetDescr(fc_onerel));
					if (fc_map != NULL)
					{
						int			fc_j;

						for (fc_j = 0; fc_j < fc_childrows; fc_j++)
						{
							HeapTuple	fc_newtup;

							fc_newtup = execute_attr_map_tuple(fc_rows[fc_numrows + fc_j], fc_map);
							heap_freetuple(fc_rows[fc_numrows + fc_j]);
							fc_rows[fc_numrows + fc_j] = fc_newtup;
						}
						free_conversion_map(fc_map);
					}
				}

				/* 并添加到计数中 */
				fc_numrows += fc_childrows;
				*fc_totalrows += fc_trows;
				*fc_totaldeadrows += fc_tdrows;
			}
		}

		/*
		 * 注意：我们无法释放子表锁，因为我们可能在抽样行中持有
		 * 指向其 TOAST 表的指针。
		 */
		table_close(fc_childrel, NoLock);
		pgstat_progress_update_param(PROGRESS_ANALYZE_CHILD_TABLES_DONE,
									 fc_i + 1);
	}

	return fc_numrows;
}


/*
 *	update_attstats() -- 更新一个关系的属性统计信息
 *
 *		统计信息存储在几个地方：关系的 pg_class 行具有关于整个关系的统计信息，
 *		每个（非系统）属性都有一个 pg_statistic 行，这些属性曾经
 *		被分析过。pg_class 值由 VACUUM 更新，而不是在这里更新。
 *
 *		pg_statistic 行会被正常添加或更新。这意味着在
 *		一个 VACUUM 周期完成时，pg_statistic 可能包含一些已删除的行，
 *		除非它恰好最后被清理。
 *
 *		为了保持简单，我们对 pg_statistic 采取不做处理的策略，并且不尝试
 *		在 pg_statistic 中为 pg_statistic 本身计算或存储行。
 *		这可能可以实现，但不值得费力去做。
 *		注意 analyze_rel() 已确保我们在
 *		清理 pg_statistic 本身时不会来到这里。
 *
 *		注意：如果两个后端可以同时 ANALYZE 相同的表，则会发生竞争条件。
 *		目前，我们通过在 analyze_rel() 中对关系进行自排他锁来避免这种情况。
 */
static void fc_update_attstats(Oid fc_relid, bool fc_inh, int fc_natts, VacAttrStats **fc_vacattrstats)
{
	Relation	fc_sd;
	int			fc_attno;

	if (fc_natts <= 0)
		return;					/* 无需操作 */

	fc_sd = table_open(StatisticRelationId, RowExclusiveLock);

	for (fc_attno = 0; fc_attno < fc_natts; fc_attno++)
	{
		VacAttrStats *fc_stats = fc_vacattrstats[fc_attno];
		HeapTuple	fc_stup,
					fc_oldtup;
		int			fc_i,
					fc_k,
					fc_n;
		Datum		fc_values[Natts_pg_statistic];
		bool		fc_nulls[Natts_pg_statistic];
		bool		fc_replaces[Natts_pg_statistic];

		/* 如果我们无法收集统计信息，则忽略属性 */
		if (!fc_stats->stats_valid)
			continue;

		/*
		 * 构造一个新的 pg_statistic 元组
		 */
		for (fc_i = 0; fc_i < Natts_pg_statistic; ++fc_i)
		{
			fc_nulls[fc_i] = false;
			fc_replaces[fc_i] = true;
		}

		fc_values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(fc_relid);
		fc_values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(fc_stats->attr->attnum);
		fc_values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(fc_inh);
		fc_values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(fc_stats->stanullfrac);
		fc_values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(fc_stats->stawidth);
		fc_values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(fc_stats->stadistinct);
		fc_i = Anum_pg_statistic_stakind1 - 1;
		for (fc_k = 0; fc_k < STATISTIC_NUM_SLOTS; fc_k++)
		{
			fc_values[fc_i++] = Int16GetDatum(fc_stats->stakind[fc_k]); /* stakindN */
		}
		fc_i = Anum_pg_statistic_staop1 - 1;
		for (fc_k = 0; fc_k < STATISTIC_NUM_SLOTS; fc_k++)
		{
			fc_values[fc_i++] = ObjectIdGetDatum(fc_stats->staop[fc_k]);	/* staopN */
		}
		fc_i = Anum_pg_statistic_stacoll1 - 1;
		for (fc_k = 0; fc_k < STATISTIC_NUM_SLOTS; fc_k++)
		{
			fc_values[fc_i++] = ObjectIdGetDatum(fc_stats->stacoll[fc_k]);	/* stacollN */
		}
		fc_i = Anum_pg_statistic_stanumbers1 - 1;
		for (fc_k = 0; fc_k < STATISTIC_NUM_SLOTS; fc_k++)
		{
			int			fc_nnum = fc_stats->numnumbers[fc_k];

			if (fc_nnum > 0)
			{
				Datum	   *fc_numdatums = (Datum *) palloc(fc_nnum * sizeof(Datum));
				ArrayType  *fc_arry;

				for (fc_n = 0; fc_n < fc_nnum; fc_n++)
					fc_numdatums[fc_n] = Float4GetDatum(fc_stats->stanumbers[fc_k][fc_n]);
				/* XXX 对 float4 类型知道的太多： */
				fc_arry = construct_array(fc_numdatums, fc_nnum,
									   FLOAT4OID,
									   sizeof(float4), true, TYPALIGN_INT);
				fc_values[fc_i++] = PointerGetDatum(fc_arry);	/* stanumbersN */
			}
			else
			{
				fc_nulls[fc_i] = true;
				fc_values[fc_i++] = (Datum) 0;
			}
		}
		fc_i = Anum_pg_statistic_stavalues1 - 1;
		for (fc_k = 0; fc_k < STATISTIC_NUM_SLOTS; fc_k++)
		{
			if (fc_stats->numvalues[fc_k] > 0)
			{
				ArrayType  *fc_arry;

				fc_arry = construct_array(fc_stats->stavalues[fc_k],
									   fc_stats->numvalues[fc_k],
									   fc_stats->statypid[fc_k],
									   fc_stats->statyplen[fc_k],
									   fc_stats->statypbyval[fc_k],
									   fc_stats->statypalign[fc_k]);
				fc_values[fc_i++] = PointerGetDatum(fc_arry);	/* stavaluesN */
			}
			else
			{
				fc_nulls[fc_i] = true;
				fc_values[fc_i++] = (Datum) 0;
			}
		}

		/* 这个属性已经有 pg_statistic 元组了吗？ */
		fc_oldtup = SearchSysCache3(STATRELATTINH,
								 ObjectIdGetDatum(fc_relid),
								 Int16GetDatum(fc_stats->attr->attnum),
								 BoolGetDatum(fc_inh));

		if (HeapTupleIsValid(fc_oldtup))
		{
			/* 是的，替换它 */
			fc_stup = heap_modify_tuple(fc_oldtup,
									 RelationGetDescr(fc_sd),
									 fc_values,
									 fc_nulls,
									 fc_replaces);
			ReleaseSysCache(fc_oldtup);
			CatalogTupleUpdate(fc_sd, &fc_stup->t_self, fc_stup);
		}
		else
		{
			/* 不是，插入新的元组 */
			fc_stup = heap_form_tuple(RelationGetDescr(fc_sd), fc_values, fc_nulls);
			CatalogTupleInsert(fc_sd, fc_stup);
		}

		heap_freetuple(fc_stup);
	}

	table_close(fc_sd, RowExclusiveLock);
}

/*
 * 计算统计子例程使用的标准获取函数。
 *
 * 这个函数存在的目的是为了在 compute_stats 例程和
 * 实际的样本数据存储之间提供一些隔离。
 */
static Datum fc_std_fetch_func(VacAttrStatsP fc_stats, int fc_rownum, bool *fc_isNull)
{
	int			fc_attnum = fc_stats->tupattnum;
	HeapTuple	fc_tuple = fc_stats->rows[fc_rownum];
	TupleDesc	fc_tupDesc = fc_stats->tupDesc;

	return heap_getattr(fc_tuple, fc_attnum, fc_tupDesc, fc_isNull);
}

/*
 * 用于分析索引表达式的获取函数。
 *
 * 我们没有费心构造索引元组，而是将数据直接放在 Datum 数组中。
 */
static Datum fc_ind_fetch_func(VacAttrStatsP fc_stats, int fc_rownum, bool *fc_isNull)
{
	int			fc_i;

	/* exprvals 和 exprnulls 已经偏移以适应适当的列 */
	fc_i = fc_rownum * fc_stats->rowstride;
	*fc_isNull = fc_stats->exprnulls[fc_i];
	return fc_stats->exprvals[fc_i];
}


/*==========================================================================
 *
 * 此点以下的代码代表“标准”特定类型统计分析算法。
 * 这段代码可以在每种数据类型的基础上通过在 pg_type.typanalyze 中
 * 设置非零值来替换。
 *
 *==========================================================================
 */


/*
 * 为了避免在分析期间消耗过多内存和/或在生成的 pg_statistic 行中
 * 占用过多空间，我们忽略宽度超过 WIDTH_THRESHOLD 的 varlena 数据项
 * （去除膨胀后！）。这对于 MCV 和不同值的计算是合理的，因为宽值
 * 很可能不会重复，更不可能是最常见的值。出于同样的原因，忽略宽值
 * 对我们的直方图区间边界估计不会产生太大影响。
 */
#define WIDTH_THRESHOLD  1024

#define swapInt(a,b)	do {int fc__tmp; fc__tmp=a; a=b; b=fc__tmp;} while(0)
#define swapDatum(a,b)	do {Datum fc__tmp; fc__tmp=a; a=b; b=fc__tmp;} while(0)

/*
 * 默认分析例程使用的额外信息
 */
typedef struct
{
	int			count;			/* 重复项数量 */
	int			first;			/* values[] 的第一次出现的索引 */
} ScalarMCVItem;

typedef struct
{
	SortSupport ssup;
	int		   *tupnoLink;
} CompareScalarsContext;


static void fc_compute_trivial_stats(VacAttrStatsP fc_stats,
								  AnalyzeAttrFetchFunc fc_fetchfunc,
								  int fc_samplerows,
								  double fc_totalrows);
static void fc_compute_distinct_stats(VacAttrStatsP fc_stats,
								   AnalyzeAttrFetchFunc fc_fetchfunc,
								   int fc_samplerows,
								   double fc_totalrows);
static void fc_compute_scalar_stats(VacAttrStatsP fc_stats,
								 AnalyzeAttrFetchFunc fc_fetchfunc,
								 int fc_samplerows,
								 double fc_totalrows);
static int	fc_compare_scalars(const void *fc_a, const void *fc_b, void *fc_arg);
static int	fc_compare_mcvs(const void *fc_a, const void *fc_b, void *fc_arg);
static int	fc_analyze_mcv_list(int *fc_mcv_counts,
							 int fc_num_mcv,
							 double fc_stadistinct,
							 double fc_stanullfrac,
							 int fc_samplerows,
							 double fc_totalrows);


/*
 * std_typanalyze -- 默认的特定类型 typanalyze 函数
 */
bool std_typanalyze(VacAttrStats *fc_stats)
{
	Form_pg_attribute fc_attr = fc_stats->attr;
	Oid			fc_ltopr;
	Oid			fc_eqopr;
	StdAnalyzeData *fc_mystats;

	/* 如果 attstattarget 列为负值，则使用默认值 */
	/* 注意：在 stats->attr 上涂写是可以的，因为这是一个副本 */
	if (fc_attr->attstattarget < 0)
		fc_attr->attstattarget = default_statistics_target;

	/* 查找列类型的默认 "<" 和 "=" 操作符 */
	get_sort_group_operators(fc_stats->attrtypid,
							 false, false, false,
							 &fc_ltopr, &fc_eqopr, NULL,
							 NULL);

	/* 为 compute_stats 例程保存操作符信息 */
	fc_mystats = (StdAnalyzeData *) palloc(sizeof(StdAnalyzeData));
	fc_mystats->eqopr = fc_eqopr;
	fc_mystats->eqfunc = OidIsValid(fc_eqopr) ? get_opcode(fc_eqopr) : InvalidOid;
	fc_mystats->ltopr = fc_ltopr;
	fc_stats->extra_data = fc_mystats;

	/*
	 * 确定使用哪种标准统计算法
	 */
	if (OidIsValid(fc_eqopr) && OidIsValid(fc_ltopr))
	{
		/* 似乎是标量数据类型 */
		fc_stats->compute_stats = fc_compute_scalar_stats;
		/*--------------------
		 * 以下对minrows的选择基于论文
		 * "随机抽样用于直方图构建：多少才够？"
		 * 由Surajit Chaudhuri, Rajeev Motwani和Vivek Narasayya撰写，
		 * 发表在1998年ACM SIGMOD国际数据管理大会的会议录中，
		 * 页码436-447。他们对定理5的推论1
		 * 说，对于表大小n，直方图大小k，箱子大小的最大相对误差f，
		 * 和误差概率gamma，最小随机样本大小为
		 *		r = 4 * k * ln(2*n/gamma) / f^2
		 * 取f = 0.5，gamma = 0.01，n = 10^6行，得到
		 *		r = 305.82 * k
		 * 注意由于对数函数，n的依赖性相当弱；即使在n = 10^12时，300*k的样本也能以0.99的概率给出<= 0.66
		 * 箱子大小误差。因此，实际上没有必要对n进行缩放，这是一件好事，因为我们此时不一定知道它。
		 *--------------------
		 */
		fc_stats->minrows = 300 * fc_attr->attstattarget;
	}
	else if (OidIsValid(fc_eqopr))
	{
		/* 我们仍然可以识别出不同的值 */
		fc_stats->compute_stats = fc_compute_distinct_stats;
		/* 不妨使用与上述相同的minrows */
		fc_stats->minrows = 300 * fc_attr->attstattarget;
	}
	else
	{
		/* 只能做一些琐碎的事情 */
		fc_stats->compute_stats = fc_compute_trivial_stats;
		/* 不妨使用与上述相同的minrows */
		fc_stats->minrows = 300 * fc_attr->attstattarget;
	}

	return true;
}


/*
 *	compute_trivial_stats() -- 计算非常基本的列统计信息
 *
 *	当我们无法为数据类型找到哈希“＝”运算符时使用此方法。
 *
 *	我们确定非空行的比例和平均数据宽度。
 */
static void fc_compute_trivial_stats(VacAttrStatsP fc_stats,
					  AnalyzeAttrFetchFunc fc_fetchfunc,
					  int fc_samplerows,
					  double fc_totalrows)
{
	int			fc_i;
	int			fc_null_cnt = 0;
	int			fc_nonnull_cnt = 0;
	double		fc_total_width = 0;
	bool		fc_is_varlena = (!fc_stats->attrtype->typbyval &&
							  fc_stats->attrtype->typlen == -1);
	bool		fc_is_varwidth = (!fc_stats->attrtype->typbyval &&
							   fc_stats->attrtype->typlen < 0);

	for (fc_i = 0; fc_i < fc_samplerows; fc_i++)
	{
		Datum		fc_value;
		bool		fc_isnull;

		vacuum_delay_point();

		fc_value = fc_fetchfunc(fc_stats, fc_i, &fc_isnull);

		/* 检查空值/非空值 */
		if (fc_isnull)
		{
			fc_null_cnt++;
			continue;
		}
		fc_nonnull_cnt++;

		/*
		 * 如果是可变宽度字段，则累加宽度以进行平均宽度
		 * 计算。注意，如果值是经过压缩的，我们使用压缩后的
		 * 宽度。如果它是固定宽度类型，我们不再计算此值。
		 */
		if (fc_is_varlena)
		{
			fc_total_width += VARSIZE_ANY(DatumGetPointer(fc_value));
		}
		else if (fc_is_varwidth)
		{
			/* 必须是cstring */
			fc_total_width += strlen(DatumGetCString(fc_value)) + 1;
		}
	}

	/* 只有在找到一些非空值时，我们才能计算平均宽度。 */
	if (fc_nonnull_cnt > 0)
	{
		fc_stats->stats_valid = true;
		/* 进行简单的空值比例和宽度统计 */
		fc_stats->stanullfrac = (double) fc_null_cnt / (double) fc_samplerows;
		if (fc_is_varwidth)
			fc_stats->stawidth = fc_total_width / (double) fc_nonnull_cnt;
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;
		fc_stats->stadistinct = 0.0;	/* "unknown" */
	}
	else if (fc_null_cnt > 0)
	{
		/* 我们只找到空值；假设该列完全为空 */
		fc_stats->stats_valid = true;
		fc_stats->stanullfrac = 1.0;
		if (fc_is_varwidth)
			fc_stats->stawidth = 0;	/* "unknown" */
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;
		fc_stats->stadistinct = 0.0;	/* "unknown" */
	}
}


/*
 *	compute_distinct_stats() -- 计算列统计信息，包括ndistinct
 *
 *	当我们只能为数据类型找到“＝”运算符时使用此方法。
 *
 *	我们确定非空行的比例、平均宽度、最常见的值，以及（估计的）不同值的数量。
 *
 *	最常见的值通过暴力算法确定：我们保留一个已看到值的列表，
 *	按出现次数排序，在扫描样本时。新看到的值插入在最后一次
 *	出现值之后，导致底部（最旧）仅出现一次的值从列表中删除。
 *	这种方法的准确性以及其成本，主要取决于我们愿意保留的列表长度。
 */
static void fc_compute_distinct_stats(VacAttrStatsP fc_stats,
					   AnalyzeAttrFetchFunc fc_fetchfunc,
					   int fc_samplerows,
					   double fc_totalrows)
{
	int			fc_i;
	int			fc_null_cnt = 0;
	int			fc_nonnull_cnt = 0;
	int			fc_toowide_cnt = 0;
	double		fc_total_width = 0;
	bool		fc_is_varlena = (!fc_stats->attrtype->typbyval &&
							  fc_stats->attrtype->typlen == -1);
	bool		fc_is_varwidth = (!fc_stats->attrtype->typbyval &&
							   fc_stats->attrtype->typlen < 0);
	FmgrInfo	fc_f_cmpeq;
	typedef struct
	{
		Datum		value;
		int			count;
	} TrackItem;
	TrackItem  *fc_track;
	int			fc_track_cnt,
				fc_track_max;
	int			fc_num_mcv = fc_stats->attr->attstattarget;
	StdAnalyzeData *fc_mystats = (StdAnalyzeData *) fc_stats->extra_data;

	/*
	 * 我们最多跟踪2*n个值用于n元素的MCV列表；但至少10个
	 */
	fc_track_max = 2 * fc_num_mcv;
	if (fc_track_max < 10)
		fc_track_max = 10;
	fc_track = (TrackItem *) palloc(fc_track_max * sizeof(TrackItem));
	fc_track_cnt = 0;

	fmgr_info(fc_mystats->eqfunc, &fc_f_cmpeq);

	for (fc_i = 0; fc_i < fc_samplerows; fc_i++)
	{
		Datum		fc_value;
		bool		fc_isnull;
		bool		fc_match;
		int			fc_firstcount1,
					fc_j;

		vacuum_delay_point();

		fc_value = fc_fetchfunc(fc_stats, fc_i, &fc_isnull);

		/* 检查空值/非空值 */
		if (fc_isnull)
		{
			fc_null_cnt++;
			continue;
		}
		fc_nonnull_cnt++;

		/*
		 * 如果是可变宽度字段，则累加宽度以进行平均宽度
		 * 计算。注意，如果值是经过压缩的，我们使用压缩后的
		 * 宽度。如果它是固定宽度类型，我们不再计算此值。
		 */
		if (fc_is_varlena)
		{
			fc_total_width += VARSIZE_ANY(DatumGetPointer(fc_value));

			/*
			 * 如果值是 toasted 的，我们希望只对其进行一次解 toast
			 * 以避免在比较过程中反复解 toast 和导致的过度内存使用。
			 * 另外，检查值是否过宽，如果过宽则根本不解 toast —— 
			 * 只需忽略该值。
			 */
			if (toast_raw_datum_size(fc_value) > WIDTH_THRESHOLD)
			{
				fc_toowide_cnt++;
				continue;
			}
			fc_value = PointerGetDatum(PG_DETOAST_DATUM(fc_value));
		}
		else if (fc_is_varwidth)
		{
			/* 必须是cstring */
			fc_total_width += strlen(DatumGetCString(fc_value)) + 1;
		}

		/*
		 * 查看值是否与我们已经跟踪的任何内容匹配。
		 */
		fc_match = false;
		fc_firstcount1 = fc_track_cnt;
		for (fc_j = 0; fc_j < fc_track_cnt; fc_j++)
		{
			if (DatumGetBool(FunctionCall2Coll(&fc_f_cmpeq,
											   fc_stats->attrcollid,
											   fc_value, fc_track[fc_j].value)))
			{
				fc_match = true;
				break;
			}
			if (fc_j < fc_firstcount1 && fc_track[fc_j].count == 1)
				fc_firstcount1 = fc_j;
		}

		if (fc_match)
		{
			/* 找到匹配项 */
			fc_track[fc_j].count++;
			/* 该值现在可能需要在跟踪列表中“上浮” */
			while (fc_j > 0 && fc_track[fc_j].count > fc_track[fc_j - 1].count)
			{
				swapDatum(fc_track[fc_j].value, fc_track[fc_j - 1].value);
				swapInt(fc_track[fc_j].count, fc_track[fc_j - 1].count);
				fc_j--;
			}
		}
		else
		{
			/* 没有匹配项。将其插入计数-1 列表的头部 */
			if (fc_track_cnt < fc_track_max)
				fc_track_cnt++;
			for (fc_j = fc_track_cnt - 1; fc_j > fc_firstcount1; fc_j--)
			{
				fc_track[fc_j].value = fc_track[fc_j - 1].value;
				fc_track[fc_j].count = fc_track[fc_j - 1].count;
			}
			if (fc_firstcount1 < fc_track_cnt)
			{
				fc_track[fc_firstcount1].value = fc_value;
				fc_track[fc_firstcount1].count = 1;
			}
		}
	}

	/* 只有在找到一些非空值的情况下，我们才能计算真实的统计数据。 */
	if (fc_nonnull_cnt > 0)
	{
		int			fc_nmultiple,
					fc_summultiple;

		fc_stats->stats_valid = true;
		/* 进行简单的空值比例和宽度统计 */
		fc_stats->stanullfrac = (double) fc_null_cnt / (double) fc_samplerows;
		if (fc_is_varwidth)
			fc_stats->stawidth = fc_total_width / (double) fc_nonnull_cnt;
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;

		/* 计算我们找到的重复值的数量 */
		fc_summultiple = 0;
		for (fc_nmultiple = 0; fc_nmultiple < fc_track_cnt; fc_nmultiple++)
		{
			if (fc_track[fc_nmultiple].count == 1)
				break;
			fc_summultiple += fc_track[fc_nmultiple].count;
		}

		if (fc_nmultiple == 0)
		{
			/*
			 * 如果我们没有找到重复的非空值，假设这是一个唯一
			 * 列；但请确保排除我们发现的任何空值。
			 */
			fc_stats->stadistinct = -1.0 * (1.0 - fc_stats->stanullfrac);
		}
		else if (fc_track_cnt < fc_track_max && fc_toowide_cnt == 0 &&
				 fc_nmultiple == fc_track_cnt)
		{
			/*
			 * 我们的跟踪列表包括样本中的每个值，并且每个
			 * 值出现超过一次。假设该列仅包含这些值。
			 * （该情况旨在处理具有小的、固定的可能值集合的列，
			 * 例如布尔或枚举列。如果样本中有任何值仅出现一次，
			 * 包括过宽的值，我们应该假设这不是我们要处理的情况。）
			 */
			fc_stats->stadistinct = fc_track_cnt;
		}
		else
		{
			/*----------
			 * 使用 Haas 和 Stokes 在 IBM 研究报告 RJ 10025 中提出的
			 * 估算器来估算不同值的数量：
			 *		n*d / (n - f1 + f1*n/N)
			 * 其中 f1 是在我们 n 行样本中恰好出现一次的不同值的数量
			 * （总共 N），d 是样本中不同值的总数。
			 * 这是他们的 Duj1 估算器；他们推荐的其他估算器
			 * 复杂得多，并且当 n 远小于 N 时数值非常不稳定。
			 *
			 * 在此计算中，我们只考虑非空值。我们以前
			 * 包含带空值的行在 n 和 N 计数中，但这
			 * 导致在许多空值的列中得出不准确的答案，而且
			 * 考虑到所需的结果是不同的非空值的数量
			 * 也是直观上错误的。
			 *
			 * 我们假设（并不可靠！）所有重复出现的
			 * 值都反映在最终的 track[] 列表中，其他
			 * 非空值都只出现过一次。（XXX 这通常
			 * 导致对 ndistinct 的严重高估。我们能
			 * 做得更好么？）
			 *----------
			 */
			int			fc_f1 = fc_nonnull_cnt - fc_summultiple;
			int			fc_d = fc_f1 + fc_nmultiple;
			double		fc_n = fc_samplerows - fc_null_cnt;
			double		fc_N = fc_totalrows * (1.0 - fc_stats->stanullfrac);
			double		fc_stadistinct;

			/* N == 0 不应该发生，但以防万一... */
			if (fc_N > 0)
				fc_stadistinct = (fc_n * fc_d) / ((fc_n - fc_f1) + fc_f1 * fc_n / fc_N);
			else
				fc_stadistinct = 0;

			/* 在发生舍入误差的情况下，限制在合理范围内 */
			if (fc_stadistinct < fc_d)
				fc_stadistinct = fc_d;
			if (fc_stadistinct > fc_N)
				fc_stadistinct = fc_N;
			/* 并四舍五入为整数 */
			fc_stats->stadistinct = floor(fc_stadistinct + 0.5);
		}

		/*
		 * 如果我们估计不同值的数量超过总行数的10%（一个非常任意的限制），
		 * 则假设stadistinct应该随着行数的变化而变化，而不是固定值。
		 */
		if (fc_stats->stadistinct > 0.1 * fc_totalrows)
			fc_stats->stadistinct = -(fc_stats->stadistinct / fc_totalrows);

		/*
		 * 决定有多少值值得作为最常见值存储。如果我们能够生成一个完整的MCV列表
		 * （样本中的所有值都可以适应，我们认为这些是表中的所有值），
		 * 那么就这样做。否则，仅存储那些显著比不在列表中的值更常见的值。
		 *
		 * 注意：这些情况中的第一种意在处理具有小且固定的可能值集合的列，
		 * 例如布尔型或枚举列。如果我们能够*完全*通过一个符合统计目标的MCV列表
		 * 来表示列的数据，那么我们应该这样做，从而为计划者提供完整的信息。
		 * 但是，如果MCV列表不完整，通常值得更具选择性，而不仅仅是填满统计目标。
		 */
		if (fc_track_cnt < fc_track_max && fc_toowide_cnt == 0 &&
			fc_stats->stadistinct > 0 &&
			fc_track_cnt <= fc_num_mcv)
		{
			/* 跟踪列表包括所有看到的值，所有值都能适应 */
			fc_num_mcv = fc_track_cnt;
		}
		else
		{
			int		   *fc_mcv_counts;

			/* 不完整列表；决定有多少值值得保留 */
			if (fc_num_mcv > fc_track_cnt)
				fc_num_mcv = fc_track_cnt;

			if (fc_num_mcv > 0)
			{
				fc_mcv_counts = (int *) palloc(fc_num_mcv * sizeof(int));
				for (fc_i = 0; fc_i < fc_num_mcv; fc_i++)
					fc_mcv_counts[fc_i] = fc_track[fc_i].count;

				fc_num_mcv = fc_analyze_mcv_list(fc_mcv_counts, fc_num_mcv,
										   fc_stats->stadistinct,
										   fc_stats->stanullfrac,
										   fc_samplerows, fc_totalrows);
			}
		}

		/* 生成MCV槽条目 */
		if (fc_num_mcv > 0)
		{
			MemoryContext fc_old_context;
			Datum	   *fc_mcv_values;
			float4	   *fc_mcv_freqs;

			/* 必须将目标值复制到anl_context中 */
			fc_old_context = MemoryContextSwitchTo(fc_stats->anl_context);
			fc_mcv_values = (Datum *) palloc(fc_num_mcv * sizeof(Datum));
			fc_mcv_freqs = (float4 *) palloc(fc_num_mcv * sizeof(float4));
			for (fc_i = 0; fc_i < fc_num_mcv; fc_i++)
			{
				fc_mcv_values[fc_i] = datumCopy(fc_track[fc_i].value,
										  fc_stats->attrtype->typbyval,
										  fc_stats->attrtype->typlen);
				fc_mcv_freqs[fc_i] = (double) fc_track[fc_i].count / (double) fc_samplerows;
			}
			MemoryContextSwitchTo(fc_old_context);

			fc_stats->stakind[0] = STATISTIC_KIND_MCV;
			fc_stats->staop[0] = fc_mystats->eqopr;
			fc_stats->stacoll[0] = fc_stats->attrcollid;
			fc_stats->stanumbers[0] = fc_mcv_freqs;
			fc_stats->numnumbers[0] = fc_num_mcv;
			fc_stats->stavalues[0] = fc_mcv_values;
			fc_stats->numvalues[0] = fc_num_mcv;

			/*
			 * 接受stats->statypid和其他的默认值。它们在我们被调用之前
			 * 已经设置（见vacuum.h）
			 */
		}
	}
	else if (fc_null_cnt > 0)
	{
		/* 我们只找到空值；假设该列完全为空 */
		fc_stats->stats_valid = true;
		fc_stats->stanullfrac = 1.0;
		if (fc_is_varwidth)
			fc_stats->stawidth = 0;	/* "unknown" */
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;
		fc_stats->stadistinct = 0.0;	/* "unknown" */
	}

	/* 我们不需要清理任何临时的palloc */
}


/*
 *	compute_scalar_stats() -- 计算列统计信息
 *
 *	当我们能够找到数据类型的“=”和“<”操作符时使用。
 *
 *	我们确定非空行的比例、平均宽度、最常见值、（估算的）不同值的数量、
 *	分布直方图以及物理顺序与逻辑顺序的相关性。
 *
 *	所需的统计信息在对数据值进行排序后可以相对容易地确定。
 */
static void fc_compute_scalar_stats(VacAttrStatsP fc_stats,
					 AnalyzeAttrFetchFunc fc_fetchfunc,
					 int fc_samplerows,
					 double fc_totalrows)
{
	int			fc_i;
	int			fc_null_cnt = 0;
	int			fc_nonnull_cnt = 0;
	int			fc_toowide_cnt = 0;
	double		fc_total_width = 0;
	bool		fc_is_varlena = (!fc_stats->attrtype->typbyval &&
							  fc_stats->attrtype->typlen == -1);
	bool		fc_is_varwidth = (!fc_stats->attrtype->typbyval &&
							   fc_stats->attrtype->typlen < 0);
	double		fc_corr_xysum;
	SortSupportData fc_ssup;
	ScalarItem *fc_values;
	int			fc_values_cnt = 0;
	int		   *fc_tupnoLink;
	ScalarMCVItem *fc_track;
	int			fc_track_cnt = 0;
	int			fc_num_mcv = fc_stats->attr->attstattarget;
	int			fc_num_bins = fc_stats->attr->attstattarget;
	StdAnalyzeData *fc_mystats = (StdAnalyzeData *) fc_stats->extra_data;

	fc_values = (ScalarItem *) palloc(fc_samplerows * sizeof(ScalarItem));
	fc_tupnoLink = (int *) palloc(fc_samplerows * sizeof(int));
	fc_track = (ScalarMCVItem *) palloc(fc_num_mcv * sizeof(ScalarMCVItem));

	memset(&fc_ssup, 0, sizeof(fc_ssup));
	fc_ssup.ssup_cxt = CurrentMemoryContext;
	fc_ssup.ssup_collation = fc_stats->attrcollid;
	fc_ssup.ssup_nulls_first = false;

	/*
	 * 目前，暂不执行缩略键转换，因为生成MCV槽所需的是完整值。
	 * 支持该优化将需要教compare_scalars()调用一个平局打破者。
	 */
	fc_ssup.abbreviate = false;

	PrepareSortSupportFromOrderingOp(fc_mystats->ltopr, &fc_ssup);

	/* 初始扫描以查找可排序的值 */
	for (fc_i = 0; fc_i < fc_samplerows; fc_i++)
	{
		Datum		fc_value;
		bool		fc_isnull;

		vacuum_delay_point();

		fc_value = fc_fetchfunc(fc_stats, fc_i, &fc_isnull);

		/* 检查空值/非空值 */
		if (fc_isnull)
		{
			fc_null_cnt++;
			continue;
		}
		fc_nonnull_cnt++;

		/*
		 * 如果是可变宽度字段，则累加宽度以进行平均宽度
		 * 计算。注意，如果值是经过压缩的，我们使用压缩后的
		 * 宽度。如果它是固定宽度类型，我们不再计算此值。
		 */
		if (fc_is_varlena)
		{
			fc_total_width += VARSIZE_ANY(DatumGetPointer(fc_value));

			/*
			 * 如果值是 toasted 的，我们希望只对其进行一次解 toast
			 * 以避免在比较过程中反复解 toast 和导致的过度内存使用。
			 * 另外，检查值是否过宽，如果过宽则根本不解 toast —— 
			 * 只需忽略该值。
			 */
			if (toast_raw_datum_size(fc_value) > WIDTH_THRESHOLD)
			{
				fc_toowide_cnt++;
				continue;
			}
			fc_value = PointerGetDatum(PG_DETOAST_DATUM(fc_value));
		}
		else if (fc_is_varwidth)
		{
			/* 必须是cstring */
			fc_total_width += strlen(DatumGetCString(fc_value)) + 1;
		}

		/* 将其添加到要排序的列表中 */
		fc_values[fc_values_cnt].value = fc_value;
		fc_values[fc_values_cnt].tupno = fc_values_cnt;
		fc_tupnoLink[fc_values_cnt] = fc_values_cnt;
		fc_values_cnt++;
	}

	/* 只有在找到一些可排序的值时，我们才能计算实际统计数据。 */
	if (fc_values_cnt > 0)
	{
		int			fc_ndistinct,	/* 样本中的不同值数量 */
					fc_nmultiple,	/* 出现多次的数量 */
					fc_num_hist,
					fc_dups_cnt;
		int			fc_slot_idx = 0;
		CompareScalarsContext fc_cxt;

		/* 对收集的值进行排序 */
		fc_cxt.ssup = &fc_ssup;
		fc_cxt.tupnoLink = fc_tupnoLink;
		qsort_interruptible((void *) fc_values, fc_values_cnt, sizeof(ScalarItem),
							fc_compare_scalars, (void *) &fc_cxt);

		/*
		 * 现在按顺序扫描这些值，找出最常见的值，并
		 * 积累排序相关的统计数据。
		 *
		 * 要确定哪些是最常见的，我们首先必须计算
		 * 每个值的重复次数。重复的值在已排序列表中是相邻的，
		 * 因此一种简单的方法是比较相邻的数据值，直到找到两个不相等的值。
		 * 然而，这需要 N-1 次调用数据比较例程，这在
		 * 排序过程中已经完全冗余了。(排序算法必须在某个时刻
		 * 比较每对在排序后相邻的项目；否则它无法知道该对是否
		 * 被正确排序。)我们利用这一点，让
		 * compare_scalars 记住每个 ScalarItem 被认为相等的
		 * 最高 tupno 索引。在排序结束时，如果且仅如果
		 * 它是其重复项组的最后一个项目，ScalarItem 的 tupnoLink
		 * 将仍然指向自身（因为该组将按 tupno 排序）。
		 */
		fc_corr_xysum = 0;
		fc_ndistinct = 0;
		fc_nmultiple = 0;
		fc_dups_cnt = 0;
		for (fc_i = 0; fc_i < fc_values_cnt; fc_i++)
		{
			int			fc_tupno = fc_values[fc_i].tupno;

			fc_corr_xysum += ((double) fc_i) * ((double) fc_tupno);
			fc_dups_cnt++;
			if (fc_tupnoLink[fc_tupno] == fc_tupno)
			{
				/* 达到了该值重复项的末尾 */
				fc_ndistinct++;
				if (fc_dups_cnt > 1)
				{
					fc_nmultiple++;
					if (fc_track_cnt < fc_num_mcv ||
						fc_dups_cnt > fc_track[fc_track_cnt - 1].count)
					{
						/*
						 * 找到了 mcv 列表的新项目；查找其
						 * 位置，如有必要，向下冒泡旧项目。循环
						 * 不变式是 j 指向一个空/可替换的
						 * 槽。
						 */
						int			fc_j;

						if (fc_track_cnt < fc_num_mcv)
							fc_track_cnt++;
						for (fc_j = fc_track_cnt - 1; fc_j > 0; fc_j--)
						{
							if (fc_dups_cnt <= fc_track[fc_j - 1].count)
								break;
							fc_track[fc_j].count = fc_track[fc_j - 1].count;
							fc_track[fc_j].first = fc_track[fc_j - 1].first;
						}
						fc_track[fc_j].count = fc_dups_cnt;
						fc_track[fc_j].first = fc_i + 1 - fc_dups_cnt;
					}
				}
				fc_dups_cnt = 0;
			}
		}

		fc_stats->stats_valid = true;
		/* 进行简单的空值比例和宽度统计 */
		fc_stats->stanullfrac = (double) fc_null_cnt / (double) fc_samplerows;
		if (fc_is_varwidth)
			fc_stats->stawidth = fc_total_width / (double) fc_nonnull_cnt;
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;

		if (fc_nmultiple == 0)
		{
			/*
			 * 如果我们没有找到重复的非空值，假设这是一个唯一
			 * 列；但请确保排除我们发现的任何空值。
			 */
			fc_stats->stadistinct = -1.0 * (1.0 - fc_stats->stanullfrac);
		}
		else if (fc_toowide_cnt == 0 && fc_nmultiple == fc_ndistinct)
		{
			/*
			 * 样本中的每个值都出现了多次。假设该列仅有这些值。
			 * （此情况旨在处理具有小的固定可能值集的列，例如
			 * 布尔值或枚举列。如果样本中出现了任何值，仅出现一次，
			 * 包括过宽的值，我们应该假设这不是我们所处理的。）
			 */
			fc_stats->stadistinct = fc_ndistinct;
		}
		else
		{
			/*----------
			 * 使用Haas和Stokes在IBM研究报告RJ 10025中提出的估计器来估计不同值的数量:
			 *		n*d / (n - f1 + f1*n/N)
			 * 其中f1是我们样本中恰好出现一次的不同值的数量，
			 * n行（总计N），d是样本中不同值的总数。
			 * 这是他们的Duj1估计器; 他们推荐的其他估计器
			 * 要复杂得多，并且当n远小于N时，在数值上非常不稳定。
			 *
			 * 在此计算中，我们仅考虑非空值。我们过去
			 * 将具有空值的行包含在n和N计数中，但这
			 * 导致在许多空值的列中得到不准确的答案，
			 * 从直觉上讲，这也不合理，因为所需结果是
			 * 非空的不同值的数量。
			 *
			 * 过宽值被假定为不同的。
			 *----------
			 */
			int			fc_f1 = fc_ndistinct - fc_nmultiple + fc_toowide_cnt;
			int			fc_d = fc_f1 + fc_nmultiple;
			double		fc_n = fc_samplerows - fc_null_cnt;
			double		fc_N = fc_totalrows * (1.0 - fc_stats->stanullfrac);
			double		fc_stadistinct;

			/* N == 0 不应该发生，但以防万一... */
			if (fc_N > 0)
				fc_stadistinct = (fc_n * fc_d) / ((fc_n - fc_f1) + fc_f1 * fc_n / fc_N);
			else
				fc_stadistinct = 0;

			/* 在发生舍入误差的情况下，限制在合理范围内 */
			if (fc_stadistinct < fc_d)
				fc_stadistinct = fc_d;
			if (fc_stadistinct > fc_N)
				fc_stadistinct = fc_N;
			/* 并四舍五入为整数 */
			fc_stats->stadistinct = floor(fc_stadistinct + 0.5);
		}

		/*
		 * 如果我们估计不同值的数量超过总行数的10%（一个非常任意的限制），
		 * 则假设stadistinct应该随着行数的变化而变化，而不是固定值。
		 */
		if (fc_stats->stadistinct > 0.1 * fc_totalrows)
			fc_stats->stadistinct = -(fc_stats->stadistinct / fc_totalrows);

		/*
		 * 决定有多少值值得作为最常见值存储。如果我们能够生成一个完整的MCV列表
		 * （样本中的所有值都可以适应，我们认为这些是表中的所有值），
		 * 那么就这样做。否则，仅存储那些显著比不在列表中的值更常见的值。
		 *
		 * 注意：这些情况中的第一种意在处理具有小且固定的可能值集合的列，
		 * 例如布尔型或枚举列。如果我们能够*完全*通过一个符合统计目标的MCV列表
		 * 来表示列的数据，那么我们应该这样做，从而为计划者提供完整的信息。
		 * 但是，如果MCV列表不完整，通常值得更具选择性，而不仅仅是填满统计目标。
		 */
		if (fc_track_cnt == fc_ndistinct && fc_toowide_cnt == 0 &&
			fc_stats->stadistinct > 0 &&
			fc_track_cnt <= fc_num_mcv)
		{
			/* 跟踪列表包括所有看到的值，所有值都能适应 */
			fc_num_mcv = fc_track_cnt;
		}
		else
		{
			int		   *fc_mcv_counts;

			/* 不完整列表；决定有多少值值得保留 */
			if (fc_num_mcv > fc_track_cnt)
				fc_num_mcv = fc_track_cnt;

			if (fc_num_mcv > 0)
			{
				fc_mcv_counts = (int *) palloc(fc_num_mcv * sizeof(int));
				for (fc_i = 0; fc_i < fc_num_mcv; fc_i++)
					fc_mcv_counts[fc_i] = fc_track[fc_i].count;

				fc_num_mcv = fc_analyze_mcv_list(fc_mcv_counts, fc_num_mcv,
										   fc_stats->stadistinct,
										   fc_stats->stanullfrac,
										   fc_samplerows, fc_totalrows);
			}
		}

		/* 生成MCV槽条目 */
		if (fc_num_mcv > 0)
		{
			MemoryContext fc_old_context;
			Datum	   *fc_mcv_values;
			float4	   *fc_mcv_freqs;

			/* 必须将目标值复制到anl_context中 */
			fc_old_context = MemoryContextSwitchTo(fc_stats->anl_context);
			fc_mcv_values = (Datum *) palloc(fc_num_mcv * sizeof(Datum));
			fc_mcv_freqs = (float4 *) palloc(fc_num_mcv * sizeof(float4));
			for (fc_i = 0; fc_i < fc_num_mcv; fc_i++)
			{
				fc_mcv_values[fc_i] = datumCopy(fc_values[fc_track[fc_i].first].value,
										  fc_stats->attrtype->typbyval,
										  fc_stats->attrtype->typlen);
				fc_mcv_freqs[fc_i] = (double) fc_track[fc_i].count / (double) fc_samplerows;
			}
			MemoryContextSwitchTo(fc_old_context);

			fc_stats->stakind[fc_slot_idx] = STATISTIC_KIND_MCV;
			fc_stats->staop[fc_slot_idx] = fc_mystats->eqopr;
			fc_stats->stacoll[fc_slot_idx] = fc_stats->attrcollid;
			fc_stats->stanumbers[fc_slot_idx] = fc_mcv_freqs;
			fc_stats->numnumbers[fc_slot_idx] = fc_num_mcv;
			fc_stats->stavalues[fc_slot_idx] = fc_mcv_values;
			fc_stats->numvalues[fc_slot_idx] = fc_num_mcv;

			/*
			 * 接受stats->statypid和其他的默认值。它们在我们被调用之前
			 * 已经设置（见vacuum.h）
			 */
			fc_slot_idx++;
		}

		/*
		 * 如果有至少两个未包含在MCV列表中的不同值，
		 * 则生成直方图槽条目。（这确保了直方图不会崩溃为空或单例。）
		 */
		fc_num_hist = fc_ndistinct - fc_num_mcv;
		if (fc_num_hist > fc_num_bins)
			fc_num_hist = fc_num_bins + 1;
		if (fc_num_hist >= 2)
		{
			MemoryContext fc_old_context;
			Datum	   *fc_hist_values;
			int			fc_nvals;
			int			fc_pos,
						fc_posfrac,
						fc_delta,
						fc_deltafrac;

			/* 将MCV项目按位置顺序排序以加快下一个循环 */
			qsort_interruptible((void *) fc_track, fc_num_mcv, sizeof(ScalarMCVItem),
								fc_compare_mcvs, NULL);

			/*
			 * 从 values[] 数组中折叠出 MCV 项目。
			 *
			 * 注意，我们在这里摧毁了 values[] 数组……但我们不需要它
			 * 用于其他任何事情。我们仍然需要 values_cnt。
			 * nvals 将是 values[] 中剩余条目的数量。
			 */
			if (fc_num_mcv > 0)
			{
				int			fc_src,
							fc_dest;
				int			fc_j;

				fc_src = fc_dest = 0;
				fc_j = 0;			/* 下一个有趣的 MCV 项目的索引 */
				while (fc_src < fc_values_cnt)
				{
					int			fc_ncopy;

					if (fc_j < fc_num_mcv)
					{
						int			fc_first = fc_track[fc_j].first;

						if (fc_src >= fc_first)
						{
							/* 跳过此 MCV 项目 */
							fc_src = fc_first + fc_track[fc_j].count;
							fc_j++;
							continue;
						}
						fc_ncopy = fc_first - fc_src;
					}
					else
						fc_ncopy = fc_values_cnt - fc_src;
					memmove(&fc_values[fc_dest], &fc_values[fc_src],
							fc_ncopy * sizeof(ScalarItem));
					fc_src += fc_ncopy;
					fc_dest += fc_ncopy;
				}
				fc_nvals = fc_dest;
			}
			else
				fc_nvals = fc_values_cnt;
			Assert(fc_nvals >= fc_num_hist);

			/* 必须将目标值复制到anl_context中 */
			fc_old_context = MemoryContextSwitchTo(fc_stats->anl_context);
			fc_hist_values = (Datum *) palloc(fc_num_hist * sizeof(Datum));

			/*
			 * 这个循环的目的是复制第一个和最后一个 values[]
			 * 条目，以及之间均匀分布的值。所以第 i 个值是
			 * values[(i * (nvals - 1)) / (num_hist - 1)]。但是
			 * 直接计算该下标存在整数溢出的风险，当
			 * 统计目标超过几千时。相反，我们
			 * 将 (nvals - 1) / (num_hist - 1) 添加到 pos 的每一步，同时跟踪
			 * 和的整数部分和小数部分。
			 */
			fc_delta = (fc_nvals - 1) / (fc_num_hist - 1);
			fc_deltafrac = (fc_nvals - 1) % (fc_num_hist - 1);
			fc_pos = fc_posfrac = 0;

			for (fc_i = 0; fc_i < fc_num_hist; fc_i++)
			{
				fc_hist_values[fc_i] = datumCopy(fc_values[fc_pos].value,
										   fc_stats->attrtype->typbyval,
										   fc_stats->attrtype->typlen);
				fc_pos += fc_delta;
				fc_posfrac += fc_deltafrac;
				if (fc_posfrac >= (fc_num_hist - 1))
				{
					/* 小数部分超过 1，将其进位到整数部分 */
					fc_pos++;
					fc_posfrac -= (fc_num_hist - 1);
				}
			}

			MemoryContextSwitchTo(fc_old_context);

			fc_stats->stakind[fc_slot_idx] = STATISTIC_KIND_HISTOGRAM;
			fc_stats->staop[fc_slot_idx] = fc_mystats->ltopr;
			fc_stats->stacoll[fc_slot_idx] = fc_stats->attrcollid;
			fc_stats->stavalues[fc_slot_idx] = fc_hist_values;
			fc_stats->numvalues[fc_slot_idx] = fc_num_hist;

			/*
			 * 接受stats->statypid和其他的默认值。它们在我们被调用之前
			 * 已经设置（见vacuum.h）
			 */
			fc_slot_idx++;
		}

		/* 如果有多个值，则生成关联条目 */
		if (fc_values_cnt > 1)
		{
			MemoryContext fc_old_context;
			float4	   *fc_corrs;
			double		fc_corr_xsum,
						fc_corr_x2sum;

			/* 必须将目标值复制到anl_context中 */
			fc_old_context = MemoryContextSwitchTo(fc_stats->anl_context);
			fc_corrs = (float4 *) palloc(sizeof(float4));
			MemoryContextSwitchTo(fc_old_context);

			/*----------
			 * 由于我们知道 x 和 y 值集都是
			 *		0，1，...，values_cnt-1
			 * 我们有 sum(x) = sum(y) =
			 *		(values_cnt-1)*values_cnt / 2
			 * 和 sum(x^2) = sum(y^2) =
			 *		(values_cnt-1)*values_cnt*(2*values_cnt-1) / 6。
			 *----------
			 */
			fc_corr_xsum = ((double) (fc_values_cnt - 1)) *
				((double) fc_values_cnt) / 2.0;
			fc_corr_x2sum = ((double) (fc_values_cnt - 1)) *
				((double) fc_values_cnt) * (double) (2 * fc_values_cnt - 1) / 6.0;

			/* 相关系数简化为 */
			fc_corrs[0] = (fc_values_cnt * fc_corr_xysum - fc_corr_xsum * fc_corr_xsum) /
				(fc_values_cnt * fc_corr_x2sum - fc_corr_xsum * fc_corr_xsum);

			fc_stats->stakind[fc_slot_idx] = STATISTIC_KIND_CORRELATION;
			fc_stats->staop[fc_slot_idx] = fc_mystats->ltopr;
			fc_stats->stacoll[fc_slot_idx] = fc_stats->attrcollid;
			fc_stats->stanumbers[fc_slot_idx] = fc_corrs;
			fc_stats->numnumbers[fc_slot_idx] = 1;
			fc_slot_idx++;
		}
	}
	else if (fc_nonnull_cnt > 0)
	{
		/* 我们发现了一些非空值，但它们都太宽了 */
		Assert(fc_nonnull_cnt == fc_toowide_cnt);
		fc_stats->stats_valid = true;
		/* 进行简单的空值比例和宽度统计 */
		fc_stats->stanullfrac = (double) fc_null_cnt / (double) fc_samplerows;
		if (fc_is_varwidth)
			fc_stats->stawidth = fc_total_width / (double) fc_nonnull_cnt;
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;
		/* 假设所有过宽的值都是唯一的，因此这是一个唯一列 */
		fc_stats->stadistinct = -1.0 * (1.0 - fc_stats->stanullfrac);
	}
	else if (fc_null_cnt > 0)
	{
		/* 我们只找到空值；假设该列完全为空 */
		fc_stats->stats_valid = true;
		fc_stats->stanullfrac = 1.0;
		if (fc_is_varwidth)
			fc_stats->stawidth = 0;	/* "unknown" */
		else
			fc_stats->stawidth = fc_stats->attrtype->typlen;
		fc_stats->stadistinct = 0.0;	/* "unknown" */
	}

	/* 我们不需要清理任何临时的palloc */
}

/*
 * 用于排序 ScalarItems 的比较器
 *
 * 除了对项目进行排序，我们还在找到两个包含相等数据的 ScalarItems 时
 * 更新 tupnoLink[] 数组。该数组通过 tupno 索引；对于每个 ScalarItem，
 * 它包含该项数据已被发现相等的最高 tupno。这使我们能够
 * 在 compute_scalar_stats() 中避免额外的比较。
 */
static int fc_compare_scalars(const void *fc_a, const void *fc_b, void *fc_arg)
{
	Datum		fc_da = ((const ScalarItem *) fc_a)->value;
	int			fc_ta = ((const ScalarItem *) fc_a)->tupno;
	Datum		fc_db = ((const ScalarItem *) fc_b)->value;
	int			fc_tb = ((const ScalarItem *) fc_b)->tupno;
	CompareScalarsContext *fc_cxt = (CompareScalarsContext *) fc_arg;
	int			fc_compare;

	fc_compare = ApplySortComparator(fc_da, false, fc_db, false, fc_cxt->ssup);
	if (fc_compare != 0)
		return fc_compare;

	/*
	 * 两个数据相等，因此更新 cxt->tupnoLink[]。
	 */
	if (fc_cxt->tupnoLink[fc_ta] < fc_tb)
		fc_cxt->tupnoLink[fc_ta] = fc_tb;
	if (fc_cxt->tupnoLink[fc_tb] < fc_ta)
		fc_cxt->tupnoLink[fc_tb] = fc_ta;

	/*
	 * 对于相等的数据，按 tupno 排序
	 */
	return fc_ta - fc_tb;
}

/*
 * 用于按位置对 ScalarMCVItems 进行排序的比较器
 */
static int fc_compare_mcvs(const void *fc_a, const void *fc_b, void *fc_arg)
{
	int			fc_da = ((const ScalarMCVItem *) fc_a)->first;
	int			fc_db = ((const ScalarMCVItem *) fc_b)->first;

	return fc_da - fc_db;
}

/*
 * 分析样本中常见值的列表，并决定值得存储在表的 MCV 列表中的数量。
 *
 * mcv_counts 假定为样本中看到的最常见值的计数列表，
 * 从最常见的开始。返回值是显著更常见的值的数量
 * 比列表中不在列表中的值更常见，因此被认为值得存储在表的 MCV 列表中。
 */
static int fc_analyze_mcv_list(int *fc_mcv_counts,
				 int fc_num_mcv,
				 double fc_stadistinct,
				 double fc_stanullfrac,
				 int fc_samplerows,
				 double fc_totalrows)
{
	double		fc_ndistinct_table;
	double		fc_sumcount;
	int			fc_i;

	/*
	 * 如果整个表被抽样，保持整个列表。这也保护我们避免下面代码中的零除错误。
	 */
	if (fc_samplerows == fc_totalrows || fc_totalrows <= 1.0)
		return fc_num_mcv;

	/* 重新提取表中非空值的估计独立数量 */
	fc_ndistinct_table = fc_stadistinct;
	if (fc_ndistinct_table < 0)
		fc_ndistinct_table = -fc_ndistinct_table * fc_totalrows;

	/*
	 * 如果最不常见的值在MCV列表中不显著更常见，则将其从MCV列表中排除。
	 * 所有非MCV值在考虑了MCV列表中所有值的频率和空值数量后，
	 * 被假定为同样常见（参见eqsel()）。
	 *
	 * 在这里，sumcount跟踪MCV列表中除了最后一个（最不常见）值的
	 * 总计数，以便我们能够确定排除该值的影响。
	 *
	 * 请注意，我们故意通过从完整列表中移除值来完成此操作，
	 * 而不是从空列表开始并添加值，因为后者的方法可能
	 * 会在所有最常见的值具有大致相同的频率并占据大多数
	 * 表时无法添加任何值，从而导致所有值的整体平均频率
	 * 大致上与常见值的频率相同。这将导致任何不常见值
	 * 被显著高估。
	 */
	fc_sumcount = 0.0;
	for (fc_i = 0; fc_i < fc_num_mcv - 1; fc_i++)
		fc_sumcount += fc_mcv_counts[fc_i];

	while (fc_num_mcv > 0)
	{
		double		fc_selec,
					fc_otherdistinct,
					fc_N,
					fc_n,
					fc_K,
					fc_variance,
					fc_stddev;

		/*
		 * 如果该值不在MCV列表中，最不常见值的估计选择性将会是（参见eqsel()）。
		 */
		fc_selec = 1.0 - fc_sumcount / fc_samplerows - fc_stanullfrac;
		if (fc_selec < 0.0)
			fc_selec = 0.0;
		if (fc_selec > 1.0)
			fc_selec = 1.0;
		fc_otherdistinct = fc_ndistinct_table - (fc_num_mcv - 1);
		if (fc_otherdistinct > 1)
			fc_selec /= fc_otherdistinct;

		/*
		 * 如果该值保留在MCV列表中，其总体频率假定等于其样本频率。
		 * 我们使用教科书的连续性校正Wald型置信区间的下限
		 * 来判断这是否显著比非MCV频率更常见——具体来说，
		 * 我们假设总体频率很可能在样本频率的约2个标准误差
		 * 之内，这相当于样本计数两侧各2个标准差的区间，
		 * 另加0.5用于连续性校正。由于我们是在不放回的情况下抽样，
		 * 这是一种超几何分布。
		 *
		 * XXX：根据经验，这种方法似乎效果很好，但值得考虑
		 * 更先进的技术来估计超几何分布的置信区间。
		 */
		fc_N = fc_totalrows;
		fc_n = fc_samplerows;
		fc_K = fc_N * fc_mcv_counts[fc_num_mcv - 1] / fc_n;
		fc_variance = fc_n * fc_K * (fc_N - fc_K) * (fc_N - fc_n) / (fc_N * fc_N * (fc_N - 1));
		fc_stddev = sqrt(fc_variance);

		if (fc_mcv_counts[fc_num_mcv - 1] > fc_selec * fc_samplerows + 2 * fc_stddev + 0.5)
		{
			/*
			 * 该值显著比非MCV选择性的建议更常见。保持它以及
			 * 列表中所有其他更常见的值。
			 */
			break;
		}
		else
		{
			/* 丢弃该值，并考虑下一个最不常见的值 */
			fc_num_mcv--;
			if (fc_num_mcv == 0)
				break;
			fc_sumcount -= fc_mcv_counts[fc_num_mcv - 1];
		}
	}
	return fc_num_mcv;
}
